mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
Basic JWT implementation (#1524)
* Basic JWT implementation * Move JWT to UI_LOGIN mode and bring back SIMPLE_LOGIN as before * Update server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com> * Refresh: Update only access token Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com> * Implement JWT Audience * Store JWT key Generates the key on startup if not set * Handle invalid Base64 * Make JWT expiry configurable * Missing value parse * Update server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com> * Simplify Duration parsing * JWT Protect Mutations * JWT Protect Queries and Subscriptions * JWT Protect v1 WebSockets * WebSockets allow sending token via protocol header * Also respect the `suwayomi-server-token` cookie * JWT reduce default token expiry * JWT Support cookie on WebSocket as well * Lint * Authenticate graphql subscription via connection_init payload * WebView: Prefer explicit token over cookie This hack was implemented because WebView sent `"null"` if no token was supplied, just don't send a bad token, then we can do this properly * WebView: Implement basic login dialog if no token supplied --------- Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com> Co-authored-by: schroda <50052685+schroda@users.noreply.github.com>
This commit is contained in:
@@ -154,6 +154,9 @@ cronUtils = "com.cronutils:cron-utils:9.2.1"
|
||||
# Webview
|
||||
kcef = "dev.datlag:kcef:2024.04.20.4"
|
||||
|
||||
# User
|
||||
jwt = "com.auth0:java-jwt:4.4.0"
|
||||
|
||||
# lint - used for renovate to update ktlint version
|
||||
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ dependencies {
|
||||
|
||||
implementation(libs.cronUtils)
|
||||
|
||||
implementation(libs.jwt)
|
||||
|
||||
compileOnly(libs.kte)
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
<string name="webview_label_loading">Loading page...</string>
|
||||
<string name="webview_label_copy">Copy to Clipboard</string>
|
||||
<string name="webview_label_copy_description">Automatic clipboard copy failed, please use the input below to manually copy the value.</string>
|
||||
<string name="webview_label_login_required">Your configuration requires you to login. Please enter username and password.</string>
|
||||
<string name="webview_placeholder_url">Enter URL...</string>
|
||||
|
||||
<string name="login_label_title">Suwayomi Login</string>
|
||||
|
||||
@@ -159,25 +159,26 @@
|
||||
main .contextmenu button:hover {
|
||||
background: #eee;
|
||||
}
|
||||
.copydialog {
|
||||
.copydialog, .logindialog {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 6px;
|
||||
z-index: 1;
|
||||
}
|
||||
.copydialog.show {
|
||||
.copydialog.show, .logindialog.show {
|
||||
display: block;
|
||||
}
|
||||
.copydialog::before {
|
||||
.copydialog::before, .logindialog::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: black;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.copydialog__inner {
|
||||
.copydialog__inner, .logindialog__inner {
|
||||
position: relative;
|
||||
max-width: 960px;
|
||||
border-radius: 8px;
|
||||
@@ -204,10 +205,10 @@
|
||||
line-height: 1;
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.copydialog {
|
||||
.copydialog, .logindialog {
|
||||
padding: 24px;
|
||||
}
|
||||
.copydialog__inner {
|
||||
.copydialog__inner, .logindialog__inner {
|
||||
padding: 12px 18px;
|
||||
height: auto;
|
||||
}
|
||||
@@ -222,6 +223,86 @@
|
||||
border-bottom: 9px solid transparent;
|
||||
border-left: 9px solid currentcolor;
|
||||
}
|
||||
|
||||
.logindialog .error {
|
||||
margin: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #b71c1c;
|
||||
background-color: #c62828;
|
||||
color: white;
|
||||
}
|
||||
.logindialog .error:empty {
|
||||
display: none;
|
||||
}
|
||||
.logindialog form label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.logindialog form button {
|
||||
all: unset;
|
||||
padding: 8px;
|
||||
line-height: 1.75;
|
||||
text-align: center;
|
||||
min-width: 64px;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
color: rgb(91, 116, 239);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02857em;
|
||||
}
|
||||
.logindialog form button:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.logindialog form button:not([disabled]):hover {
|
||||
background-color: rgba(91, 116, 239, 0.08);
|
||||
}
|
||||
.logindialog form input {
|
||||
all: unset;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.23);
|
||||
padding: 6px 12px;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.logindialog form input:hover {
|
||||
border-color: white;
|
||||
}
|
||||
.logindialog form input:focus {
|
||||
border-color: rgb(91, 116, 239);
|
||||
}
|
||||
.logindialog form .controls {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.logindialog form .controls > :nth-child(even):not(:last-child) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.logindialog form .submit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.logindialog input:disabled, .logindialog button:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.logindialog form {
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
margin: 8px auto;
|
||||
}
|
||||
.logindialog form .controls {
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: 16px;
|
||||
row-gap: 6px;
|
||||
}
|
||||
.logindialog form .controls > :nth-child(even):not(:last-child) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -256,6 +337,24 @@
|
||||
<input type="text" id="copyinput" disabled readonly/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logindialog" id="logindialog" role="dialog">
|
||||
<div class="logindialog__inner">
|
||||
<form>
|
||||
<h2>Login</h2>
|
||||
<div class="error"></div>
|
||||
<p>${MR.strings.webview_label_login_required.localized(locale)}</p>
|
||||
<div class="controls">
|
||||
<label for="user">${MR.strings.login_label_username.localized(locale)}:</label>
|
||||
<input type="text" name="user" id="user" required placeholder="${MR.strings.login_placeholder_username.localized(locale)}"/>
|
||||
<label for="pass">${MR.strings.login_label_password.localized(locale)}:</label>
|
||||
<input type="password" name="pass" id="pass" required placeholder="${MR.strings.login_placeholder_password.localized(locale)}"/>
|
||||
</div>
|
||||
<div class="submit">
|
||||
<button type="submit" disabled>${MR.strings.login_label_login.localized(locale)}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const messageDiv = document.getElementById('message');
|
||||
const statusDiv = document.getElementById('status');
|
||||
@@ -274,10 +373,89 @@
|
||||
const titleDiv = document.getElementById('title');
|
||||
const reverseToggle = document.getElementById('reverseScroll');
|
||||
const origTitle = document.title;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('token');
|
||||
|
||||
function connectWs(socketUrl, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// we pass the token as the subprotocol, which is widely considered the best solution to passing tokens
|
||||
// browsers don't support setting custom headers for WebSockets...
|
||||
const socket = new WebSocket(socketUrl, token ? [token] : []);
|
||||
const f = (msg) => {
|
||||
console.debug('Connection active:', msg.data);
|
||||
socket.removeEventListener('message', f);
|
||||
socket.removeEventListener('close', closef);
|
||||
resolve(socket);
|
||||
};
|
||||
const closef = (e) => {
|
||||
socket.removeEventListener('message', f);
|
||||
if (e.code === 1011 && e.reason === "Unauthorized") {
|
||||
const loginDiv = document.getElementById('logindialog');
|
||||
const loginForm = document.querySelector('#logindialog form');
|
||||
const loginError = document.querySelector('#logindialog .error');
|
||||
loginError.textContent = '';
|
||||
loginForm.querySelectorAll('input, button').forEach(i => i.disabled = false);
|
||||
loginForm.addEventListener('submit', async (sev) => {
|
||||
sev.preventDefault();
|
||||
loginForm.querySelectorAll('input, button').forEach(i => i.disabled = true);
|
||||
const mutation = {
|
||||
"query": "mutation LOGIN($input: LoginInput!) {\n login(input: $input) {\n accessToken\n refreshToken\n }\n}",
|
||||
"variables": {
|
||||
"input": {
|
||||
"username": loginForm.user.value,
|
||||
"password": loginForm.pass.value,
|
||||
},
|
||||
},
|
||||
"operationName": "LOGIN",
|
||||
};
|
||||
const resp = await fetch("/api/graphql", {
|
||||
"headers": {
|
||||
"Accept": "application/json, multipart/mixed",
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache"
|
||||
},
|
||||
"body": JSON.stringify(mutation),
|
||||
"method": "POST",
|
||||
}).then(r => r.json());
|
||||
if (resp.errors && resp.errors.length > 0) {
|
||||
const err = resp.errors[0].message.replace(/Exception[^:]* :|\r?\n.*/g, '');
|
||||
loginError.textContent = err;
|
||||
loginForm.pass.value = '';
|
||||
loginForm.querySelectorAll('input, button').forEach(i => i.disabled = false);
|
||||
} else {
|
||||
const newToken = resp.data.login.accessToken;
|
||||
const expiry = new Date(JSON.parse(atob(newToken.split('.')[1])).exp * 1000);
|
||||
console.log('Got new token', newToken, 'expires', expiry);
|
||||
document.cookie = "suwayomi-server-token=" + newToken + "; path=/; expires=" + expiry.toUTCString();
|
||||
loginDiv.classList.remove('show');
|
||||
loginForm.querySelectorAll('input, button').forEach(i => i.disabled = true);
|
||||
const newSocket = new WebSocket(socketUrl, [newToken]);
|
||||
newSocket.addEventListener('open', () => {
|
||||
resolve(newSocket);
|
||||
});
|
||||
}
|
||||
});
|
||||
loginDiv.classList.add('show');
|
||||
return;
|
||||
}
|
||||
socket.removeEventListener('close', closef);
|
||||
reject(e);
|
||||
};
|
||||
socket.addEventListener('open', () => {
|
||||
console.debug('Socket opened, PING');
|
||||
socket.addEventListener('message', f);
|
||||
socket.send(JSON.stringify({type: "ping"}));
|
||||
});
|
||||
socket.addEventListener('close', closef);
|
||||
});
|
||||
}
|
||||
|
||||
(async function() {
|
||||
try {
|
||||
const socketUrl = (window.location.origin + window.location.pathname).replace(/^http/,'ws');
|
||||
const socket = new WebSocket(socketUrl);
|
||||
// we pass the token as the subprotocol, which is widely considered the best solution to passing tokens
|
||||
// browsers don't support setting custom headers for WebSockets...
|
||||
const socket = await connectWs(socketUrl, token);
|
||||
|
||||
urlInput.disabled = false;
|
||||
goButton.disabled = false;
|
||||
@@ -393,11 +571,6 @@
|
||||
|
||||
/// Server events
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
loadUrl(url);
|
||||
console.log('WebSocket connection opened');
|
||||
});
|
||||
|
||||
socket.addEventListener('message', e => {
|
||||
const obj = JSON.parse(e.data);
|
||||
switch (obj.type) {
|
||||
@@ -449,7 +622,7 @@
|
||||
if (e.wasClean) {
|
||||
console.log(`WebSocket connection closed cleanly, code=` + e.code + `, reason=` + e.reason);
|
||||
} else {
|
||||
console.error('WebSocket connection died');
|
||||
console.error('WebSocket connection died', e);
|
||||
}
|
||||
document.body.classList.add('disconnected');
|
||||
});
|
||||
@@ -583,11 +756,13 @@
|
||||
};
|
||||
attachEvents();
|
||||
frameInput.focus();
|
||||
loadUrl(url);
|
||||
} catch (e) {
|
||||
messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + (e.message || e);
|
||||
messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + (e.message || e.reason || e);
|
||||
messageDiv.classList.add('error');
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,6 +9,9 @@ package suwayomi.tachidesk.global.controller
|
||||
|
||||
import io.javalin.http.HttpStatus
|
||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import suwayomi.tachidesk.server.util.formParam
|
||||
import suwayomi.tachidesk.server.util.handler
|
||||
import suwayomi.tachidesk.server.util.withOperation
|
||||
@@ -24,6 +27,7 @@ object GlobalMetaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(GlobalMeta.getMetaMap())
|
||||
ctx.status(200)
|
||||
},
|
||||
@@ -44,6 +48,7 @@ object GlobalMetaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, key, value ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
GlobalMeta.modifyMeta(key, value)
|
||||
ctx.status(200)
|
||||
},
|
||||
|
||||
@@ -12,7 +12,10 @@ import suwayomi.tachidesk.global.impl.About
|
||||
import suwayomi.tachidesk.global.impl.AboutDataClass
|
||||
import suwayomi.tachidesk.global.impl.AppUpdate
|
||||
import suwayomi.tachidesk.global.impl.UpdateDataClass
|
||||
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.withOperation
|
||||
|
||||
@@ -28,6 +31,7 @@ object SettingsController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(About.getAbout())
|
||||
},
|
||||
withResults = {
|
||||
@@ -45,6 +49,7 @@ object SettingsController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { AppUpdate.checkUpdate() }
|
||||
.thenApply { ctx.json(it) }
|
||||
|
||||
@@ -12,6 +12,9 @@ import io.javalin.http.HttpStatus
|
||||
import io.javalin.websocket.WsConfig
|
||||
import suwayomi.tachidesk.global.impl.WebView
|
||||
import suwayomi.tachidesk.i18n.LocalizationHelper
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import suwayomi.tachidesk.server.util.handler
|
||||
import suwayomi.tachidesk.server.util.queryParam
|
||||
import suwayomi.tachidesk.server.util.withOperation
|
||||
@@ -28,6 +31,7 @@ object WebViewController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, lang ->
|
||||
// intentionally not user-protected, this pages handles login by itself
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.contentType(ContentType.TEXT_HTML)
|
||||
ctx.render(
|
||||
@@ -41,8 +45,15 @@ object WebViewController {
|
||||
)
|
||||
|
||||
fun webviewWS(ws: WsConfig) {
|
||||
ws.onConnect { ctx -> WebView.addClient(ctx) }
|
||||
ws.onMessage { ctx -> WebView.handleRequest(ctx) }
|
||||
ws.onClose { ctx -> WebView.removeClient(ctx) }
|
||||
ws.onConnect { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
WebView.addClient(ctx)
|
||||
}
|
||||
ws.onMessage { ctx ->
|
||||
WebView.handleRequest(ctx)
|
||||
}
|
||||
ws.onClose { ctx ->
|
||||
WebView.removeClient(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ object WebView : Websocket<String>() {
|
||||
@SerialName("copy")
|
||||
class JsCopyMessage : TypeObject()
|
||||
|
||||
@Serializable
|
||||
@SerialName("ping")
|
||||
class JsPingMessage : TypeObject()
|
||||
|
||||
override fun handleRequest(ctx: WsMessageContext) {
|
||||
val dr = driver ?: return
|
||||
try {
|
||||
@@ -113,6 +117,9 @@ object WebView : Websocket<String>() {
|
||||
is JsCopyMessage -> {
|
||||
dr.copy()
|
||||
}
|
||||
is JsPingMessage -> {
|
||||
notifyAllClients("{\"type\":\"pong\"}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to deserialize client request: ${ctx.message()}" }
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package suwayomi.tachidesk.global.impl.util
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.JWTVerifier
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.server.user.UserType
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.security.SecureRandom
|
||||
import java.time.Instant
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
object Jwt {
|
||||
private val preferenceStore =
|
||||
Injekt.get<Application>().getSharedPreferences("jwt", Context.MODE_PRIVATE)
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private const val ALGORITHM = "HmacSHA256"
|
||||
private val accessTokenExpiry get() = serverConfig.jwtTokenExpiry.value
|
||||
private val refreshTokenExpiry get() = serverConfig.jwtRefreshExpiry.value
|
||||
private const val ISSUER = "suwayomi-server"
|
||||
private val AUDIENCE get() = serverConfig.jwtAudience.value
|
||||
|
||||
private const val PREF_KEY = "jwt_key"
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
fun generateSecret(): String {
|
||||
val byteString = preferenceStore.getString(PREF_KEY, "")
|
||||
val decodedKeyBytes =
|
||||
try {
|
||||
Base64.Default.decode(byteString)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
logger.warn(e) { "Invalid key specified, regenerating" }
|
||||
null
|
||||
}
|
||||
|
||||
val keyBytes =
|
||||
if (decodedKeyBytes?.size == 32) {
|
||||
decodedKeyBytes
|
||||
} else {
|
||||
val k = ByteArray(32)
|
||||
SecureRandom().nextBytes(k)
|
||||
preferenceStore.edit().putString(PREF_KEY, Base64.Default.encode(k)).apply()
|
||||
k
|
||||
}
|
||||
|
||||
val secretKey = SecretKeySpec(keyBytes, ALGORITHM)
|
||||
|
||||
return Base64.encode(secretKey.encoded)
|
||||
}
|
||||
|
||||
private val algorithm: Algorithm = Algorithm.HMAC256(generateSecret())
|
||||
private val verifier: JWTVerifier = JWT.require(algorithm).build()
|
||||
|
||||
class JwtTokens(
|
||||
val accessToken: String,
|
||||
val refreshToken: String,
|
||||
)
|
||||
|
||||
fun generateJwt(): JwtTokens {
|
||||
val accessToken = createAccessToken()
|
||||
val refreshToken = createRefreshToken()
|
||||
|
||||
return JwtTokens(
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
)
|
||||
}
|
||||
|
||||
fun refreshJwt(refreshToken: String): String {
|
||||
val jwt = verifier.verify(refreshToken)
|
||||
require(jwt.getClaim("token_type").asString() == "refresh") {
|
||||
"Cannot use access token to refresh"
|
||||
}
|
||||
require(jwt.audience.single() == AUDIENCE) {
|
||||
"Token intended for different audience ${jwt.audience}"
|
||||
}
|
||||
return createAccessToken()
|
||||
}
|
||||
|
||||
fun verifyJwt(jwt: String): UserType {
|
||||
try {
|
||||
val decodedJWT = verifier.verify(jwt)
|
||||
|
||||
require(decodedJWT.getClaim("token_type").asString() == "access") {
|
||||
"Cannot use refresh token to access"
|
||||
}
|
||||
require(decodedJWT.audience.single() == AUDIENCE) {
|
||||
"Token intended for different audience ${decodedJWT.audience}"
|
||||
}
|
||||
|
||||
return UserType.Admin(1)
|
||||
} catch (e: JWTVerificationException) {
|
||||
logger.warn(e) { "Received invalid token" }
|
||||
return UserType.Visitor
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAccessToken(): String {
|
||||
val jwt =
|
||||
JWT
|
||||
.create()
|
||||
.withIssuer(ISSUER)
|
||||
.withAudience(AUDIENCE)
|
||||
.withClaim("token_type", "access")
|
||||
.withExpiresAt(Instant.now().plusSeconds(accessTokenExpiry.inWholeSeconds))
|
||||
|
||||
return jwt.sign(algorithm)
|
||||
}
|
||||
|
||||
private fun createRefreshToken(): String =
|
||||
JWT
|
||||
.create()
|
||||
.withIssuer(ISSUER)
|
||||
.withAudience(AUDIENCE)
|
||||
.withClaim("token_type", "refresh")
|
||||
.withExpiresAt(Instant.now().plusSeconds(refreshTokenExpiry.inWholeSeconds))
|
||||
.sign(algorithm)
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import io.javalin.http.UploadedFile
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import suwayomi.tachidesk.graphql.server.TemporaryFileStorage
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.BackupRestoreStatus
|
||||
import suwayomi.tachidesk.graphql.types.toStatus
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -26,7 +31,11 @@ class BackupMutation {
|
||||
val status: BackupRestoreStatus?,
|
||||
)
|
||||
|
||||
fun restoreBackup(input: RestoreBackupInput): CompletableFuture<RestoreBackupPayload> {
|
||||
fun restoreBackup(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: RestoreBackupInput,
|
||||
): CompletableFuture<RestoreBackupPayload> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, backup) = input
|
||||
|
||||
return future {
|
||||
@@ -53,7 +62,11 @@ class BackupMutation {
|
||||
val url: String,
|
||||
)
|
||||
|
||||
fun createBackup(input: CreateBackupInput? = null): CreateBackupPayload {
|
||||
fun createBackup(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: CreateBackupInput? = null,
|
||||
): CreateBackupPayload {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val filename = Backup.getFilename()
|
||||
|
||||
val backup =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
|
||||
@@ -12,6 +13,7 @@ import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.CategoryMetaType
|
||||
import suwayomi.tachidesk.graphql.types.CategoryType
|
||||
import suwayomi.tachidesk.graphql.types.MangaType
|
||||
@@ -23,6 +25,9 @@ import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
|
||||
class CategoryMutation {
|
||||
data class SetCategoryMetaInput(
|
||||
@@ -35,8 +40,12 @@ class CategoryMutation {
|
||||
val meta: CategoryMetaType,
|
||||
)
|
||||
|
||||
fun setCategoryMeta(input: SetCategoryMetaInput): DataFetcherResult<SetCategoryMetaPayload?> =
|
||||
fun setCategoryMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: SetCategoryMetaInput,
|
||||
): DataFetcherResult<SetCategoryMetaPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
Category.modifyMeta(meta.categoryId, meta.key, meta.value)
|
||||
@@ -56,8 +65,12 @@ class CategoryMutation {
|
||||
val category: CategoryType,
|
||||
)
|
||||
|
||||
fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DataFetcherResult<DeleteCategoryMetaPayload?> =
|
||||
fun deleteCategoryMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DeleteCategoryMetaInput,
|
||||
): DataFetcherResult<DeleteCategoryMetaPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, categoryId, key) = input
|
||||
|
||||
val (meta, category) =
|
||||
@@ -150,8 +163,12 @@ class CategoryMutation {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategory(input: UpdateCategoryInput): DataFetcherResult<UpdateCategoryPayload?> =
|
||||
fun updateCategory(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateCategoryInput,
|
||||
): DataFetcherResult<UpdateCategoryPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
updateCategories(listOf(id), patch)
|
||||
@@ -167,8 +184,12 @@ class CategoryMutation {
|
||||
)
|
||||
}
|
||||
|
||||
fun updateCategories(input: UpdateCategoriesInput): DataFetcherResult<UpdateCategoriesPayload?> =
|
||||
fun updateCategories(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateCategoriesInput,
|
||||
): DataFetcherResult<UpdateCategoriesPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
updateCategories(ids, patch)
|
||||
@@ -195,8 +216,12 @@ class CategoryMutation {
|
||||
val position: Int,
|
||||
)
|
||||
|
||||
fun updateCategoryOrder(input: UpdateCategoryOrderInput): DataFetcherResult<UpdateCategoryOrderPayload?> =
|
||||
fun updateCategoryOrder(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateCategoryOrderInput,
|
||||
): DataFetcherResult<UpdateCategoryOrderPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, categoryId, position) = input
|
||||
require(position > 0) {
|
||||
"'order' must not be <= 0"
|
||||
@@ -253,8 +278,12 @@ class CategoryMutation {
|
||||
val category: CategoryType,
|
||||
)
|
||||
|
||||
fun createCategory(input: CreateCategoryInput): DataFetcherResult<CreateCategoryPayload?> =
|
||||
fun createCategory(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: CreateCategoryInput,
|
||||
): DataFetcherResult<CreateCategoryPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input
|
||||
transaction {
|
||||
require(CategoryTable.selectAll().where { CategoryTable.name eq input.name }.isEmpty()) {
|
||||
@@ -312,8 +341,12 @@ class CategoryMutation {
|
||||
val mangas: List<MangaType>,
|
||||
)
|
||||
|
||||
fun deleteCategory(input: DeleteCategoryInput): DataFetcherResult<DeleteCategoryPayload?> {
|
||||
fun deleteCategory(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DeleteCategoryInput,
|
||||
): DataFetcherResult<DeleteCategoryPayload?> {
|
||||
return asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, categoryId) = input
|
||||
if (categoryId == 0) { // Don't delete default category
|
||||
return@asDataFetcherResult DeleteCategoryPayload(
|
||||
@@ -401,8 +434,12 @@ class CategoryMutation {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMangaCategories(input: UpdateMangaCategoriesInput): DataFetcherResult<UpdateMangaCategoriesPayload?> =
|
||||
fun updateMangaCategories(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateMangaCategoriesInput,
|
||||
): DataFetcherResult<UpdateMangaCategoriesPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
updateMangas(listOf(id), patch)
|
||||
@@ -418,8 +455,12 @@ class CategoryMutation {
|
||||
)
|
||||
}
|
||||
|
||||
fun updateMangasCategories(input: UpdateMangasCategoriesInput): DataFetcherResult<UpdateMangasCategoriesPayload?> =
|
||||
fun updateMangasCategories(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateMangasCategoriesInput,
|
||||
): DataFetcherResult<UpdateMangasCategoriesPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
updateMangas(ids, patch)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
@@ -12,6 +13,7 @@ import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.ChapterMetaType
|
||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
|
||||
@@ -20,7 +22,10 @@ import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
||||
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
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 java.net.URLEncoder
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.CompletableFuture
|
||||
@@ -112,8 +117,12 @@ class ChapterMutation {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChapter(input: UpdateChapterInput): DataFetcherResult<UpdateChapterPayload?> =
|
||||
fun updateChapter(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateChapterInput,
|
||||
): DataFetcherResult<UpdateChapterPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
updateChapters(listOf(id), patch)
|
||||
@@ -129,8 +138,12 @@ class ChapterMutation {
|
||||
)
|
||||
}
|
||||
|
||||
fun updateChapters(input: UpdateChaptersInput): DataFetcherResult<UpdateChaptersPayload?> =
|
||||
fun updateChapters(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateChaptersInput,
|
||||
): DataFetcherResult<UpdateChaptersPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
updateChapters(ids, patch)
|
||||
@@ -156,7 +169,11 @@ class ChapterMutation {
|
||||
val chapters: List<ChapterType>,
|
||||
)
|
||||
|
||||
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<DataFetcherResult<FetchChaptersPayload?>> {
|
||||
fun fetchChapters(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: FetchChaptersInput,
|
||||
): CompletableFuture<DataFetcherResult<FetchChaptersPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, mangaId) = input
|
||||
|
||||
return future {
|
||||
@@ -190,8 +207,12 @@ class ChapterMutation {
|
||||
val meta: ChapterMetaType,
|
||||
)
|
||||
|
||||
fun setChapterMeta(input: SetChapterMetaInput): DataFetcherResult<SetChapterMetaPayload?> =
|
||||
fun setChapterMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: SetChapterMetaInput,
|
||||
): DataFetcherResult<SetChapterMetaPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
|
||||
@@ -211,8 +232,12 @@ class ChapterMutation {
|
||||
val chapter: ChapterType,
|
||||
)
|
||||
|
||||
fun deleteChapterMeta(input: DeleteChapterMetaInput): DataFetcherResult<DeleteChapterMetaPayload?> =
|
||||
fun deleteChapterMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DeleteChapterMetaInput,
|
||||
): DataFetcherResult<DeleteChapterMetaPayload?> =
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapterId, key) = input
|
||||
|
||||
val (meta, chapter) =
|
||||
@@ -260,7 +285,11 @@ class ChapterMutation {
|
||||
val syncConflict: SyncConflictInfoType?,
|
||||
)
|
||||
|
||||
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
|
||||
fun fetchChapterPages(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: FetchChapterPagesInput,
|
||||
): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapterId) = input
|
||||
val paramsMap = input.toParams()
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.graphql.types.DownloadStatus
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
@@ -13,7 +15,10 @@ import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadUpdateType.DEQUEUED
|
||||
import suwayomi.tachidesk.manga.impl.download.model.Status
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -28,7 +33,11 @@ class DownloadMutation {
|
||||
val chapters: List<ChapterType>,
|
||||
)
|
||||
|
||||
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DataFetcherResult<DeleteDownloadedChaptersPayload?> {
|
||||
fun deleteDownloadedChapters(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DeleteDownloadedChaptersInput,
|
||||
): DataFetcherResult<DeleteDownloadedChaptersPayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapters) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
@@ -57,7 +66,11 @@ class DownloadMutation {
|
||||
val chapters: ChapterType,
|
||||
)
|
||||
|
||||
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DataFetcherResult<DeleteDownloadedChapterPayload?> {
|
||||
fun deleteDownloadedChapter(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DeleteDownloadedChapterInput,
|
||||
): DataFetcherResult<DeleteDownloadedChapterPayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapter) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
@@ -84,8 +97,10 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
fun enqueueChapterDownloads(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: EnqueueChapterDownloadsInput,
|
||||
): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadsPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapters) = input
|
||||
|
||||
return future {
|
||||
@@ -118,7 +133,11 @@ class DownloadMutation {
|
||||
val downloadStatus: DownloadStatus,
|
||||
)
|
||||
|
||||
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadPayload?>> {
|
||||
fun enqueueChapterDownload(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: EnqueueChapterDownloadInput,
|
||||
): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapter) = input
|
||||
|
||||
return future {
|
||||
@@ -151,8 +170,10 @@ class DownloadMutation {
|
||||
)
|
||||
|
||||
fun dequeueChapterDownloads(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DequeueChapterDownloadsInput,
|
||||
): CompletableFuture<DataFetcherResult<DequeueChapterDownloadsPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapters) = input
|
||||
|
||||
return future {
|
||||
@@ -187,7 +208,11 @@ class DownloadMutation {
|
||||
val downloadStatus: DownloadStatus,
|
||||
)
|
||||
|
||||
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DataFetcherResult<DequeueChapterDownloadPayload?>> {
|
||||
fun dequeueChapterDownload(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DequeueChapterDownloadInput,
|
||||
): CompletableFuture<DataFetcherResult<DequeueChapterDownloadPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapter) = input
|
||||
|
||||
return future {
|
||||
@@ -221,9 +246,13 @@ class DownloadMutation {
|
||||
val downloadStatus: DownloadStatus,
|
||||
)
|
||||
|
||||
fun startDownloader(input: StartDownloaderInput): CompletableFuture<DataFetcherResult<StartDownloaderPayload?>> =
|
||||
fun startDownloader(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: StartDownloaderInput,
|
||||
): CompletableFuture<DataFetcherResult<StartDownloaderPayload?>> =
|
||||
future {
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
DownloadManager.start()
|
||||
|
||||
StartDownloaderPayload(
|
||||
@@ -249,9 +278,13 @@ class DownloadMutation {
|
||||
val downloadStatus: DownloadStatus,
|
||||
)
|
||||
|
||||
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<DataFetcherResult<StopDownloaderPayload?>> =
|
||||
fun stopDownloader(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: StopDownloaderInput,
|
||||
): CompletableFuture<DataFetcherResult<StopDownloaderPayload?>> =
|
||||
future {
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
DownloadManager.stop()
|
||||
|
||||
StopDownloaderPayload(
|
||||
@@ -277,9 +310,13 @@ class DownloadMutation {
|
||||
val downloadStatus: DownloadStatus,
|
||||
)
|
||||
|
||||
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<DataFetcherResult<ClearDownloaderPayload?>> =
|
||||
fun clearDownloader(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: ClearDownloaderInput,
|
||||
): CompletableFuture<DataFetcherResult<ClearDownloaderPayload?>> =
|
||||
future {
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
DownloadManager.clear()
|
||||
|
||||
ClearDownloaderPayload(
|
||||
@@ -307,7 +344,11 @@ class DownloadMutation {
|
||||
val downloadStatus: DownloadStatus,
|
||||
)
|
||||
|
||||
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<DataFetcherResult<ReorderChapterDownloadPayload?>> {
|
||||
fun reorderChapterDownload(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: ReorderChapterDownloadInput,
|
||||
): CompletableFuture<DataFetcherResult<ReorderChapterDownloadPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, chapter, to) = input
|
||||
|
||||
return future {
|
||||
|
||||
@@ -2,15 +2,20 @@ package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import io.javalin.http.UploadedFile
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
|
||||
class ExtensionMutation {
|
||||
@@ -73,7 +78,11 @@ class ExtensionMutation {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateExtension(input: UpdateExtensionInput): CompletableFuture<DataFetcherResult<UpdateExtensionPayload?>> {
|
||||
fun updateExtension(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateExtensionInput,
|
||||
): CompletableFuture<DataFetcherResult<UpdateExtensionPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
return future {
|
||||
@@ -97,7 +106,11 @@ class ExtensionMutation {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<DataFetcherResult<UpdateExtensionsPayload?>> {
|
||||
fun updateExtensions(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateExtensionsInput,
|
||||
): CompletableFuture<DataFetcherResult<UpdateExtensionsPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
return future {
|
||||
@@ -129,7 +142,11 @@ class ExtensionMutation {
|
||||
val extensions: List<ExtensionType>,
|
||||
)
|
||||
|
||||
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<DataFetcherResult<FetchExtensionsPayload?>> {
|
||||
fun fetchExtensions(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: FetchExtensionsInput,
|
||||
): CompletableFuture<DataFetcherResult<FetchExtensionsPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId) = input
|
||||
|
||||
return future {
|
||||
@@ -163,8 +180,10 @@ class ExtensionMutation {
|
||||
)
|
||||
|
||||
fun installExternalExtension(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: InstallExternalExtensionInput,
|
||||
): CompletableFuture<DataFetcherResult<InstallExternalExtensionPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, extensionFile) = input
|
||||
|
||||
return future {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||
@@ -21,7 +26,11 @@ class ImageMutation {
|
||||
val cachedPages: Boolean?,
|
||||
)
|
||||
|
||||
fun clearCachedImages(input: ClearCachedImagesInput): ClearCachedImagesPayload {
|
||||
fun clearCachedImages(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: ClearCachedImagesInput,
|
||||
): ClearCachedImagesPayload {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, downloadedThumbnails, cachedThumbnails, cachedPages) = input
|
||||
|
||||
val downloadedThumbnailsResult =
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
|
||||
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
|
||||
import suwayomi.tachidesk.graphql.types.UpdateState.IDLE
|
||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
||||
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
|
||||
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.WebInterfaceManager
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -24,9 +29,13 @@ class InfoMutation {
|
||||
val updateStatus: WebUIUpdateStatus,
|
||||
)
|
||||
|
||||
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<DataFetcherResult<WebUIUpdatePayload?>> {
|
||||
fun updateWebUI(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: WebUIUpdateInput,
|
||||
): CompletableFuture<DataFetcherResult<WebUIUpdatePayload?>> {
|
||||
return future {
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
withTimeout(30.seconds) {
|
||||
if (WebInterfaceManager.status.value.state === DOWNLOADING) {
|
||||
return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value)
|
||||
@@ -59,9 +68,10 @@ class InfoMutation {
|
||||
}
|
||||
}
|
||||
|
||||
fun resetWebUIUpdateStatus(): CompletableFuture<DataFetcherResult<WebUIUpdateStatus?>> =
|
||||
fun resetWebUIUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<DataFetcherResult<WebUIUpdateStatus?>> =
|
||||
future {
|
||||
asDataFetcherResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
withTimeout(30.seconds) {
|
||||
val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING
|
||||
if (!isUpdateFinished) {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload
|
||||
import suwayomi.tachidesk.graphql.types.LogoutKoSyncAccountPayload
|
||||
import suwayomi.tachidesk.graphql.types.SettingsType
|
||||
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
|
||||
class KoreaderSyncMutation {
|
||||
@@ -14,8 +19,12 @@ class KoreaderSyncMutation {
|
||||
val password: String,
|
||||
)
|
||||
|
||||
fun connectKoSyncAccount(input: ConnectKoSyncAccountInput): CompletableFuture<KoSyncConnectPayload> =
|
||||
fun connectKoSyncAccount(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: ConnectKoSyncAccountInput,
|
||||
): CompletableFuture<KoSyncConnectPayload> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val result = KoreaderSyncService.connect(input.username, input.password)
|
||||
|
||||
KoSyncConnectPayload(
|
||||
@@ -31,8 +40,12 @@ class KoreaderSyncMutation {
|
||||
val clientMutationId: String? = null,
|
||||
)
|
||||
|
||||
fun logoutKoSyncAccount(input: LogoutKoSyncAccountInput): CompletableFuture<LogoutKoSyncAccountPayload> =
|
||||
fun logoutKoSyncAccount(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: LogoutKoSyncAccountInput,
|
||||
): CompletableFuture<LogoutKoSyncAccountPayload> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
KoreaderSyncService.logout()
|
||||
LogoutKoSyncAccountPayload(
|
||||
clientMutationId = input.clientMutationId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
@@ -8,6 +9,7 @@ import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
||||
import suwayomi.tachidesk.graphql.types.MangaType
|
||||
import suwayomi.tachidesk.manga.impl.Library
|
||||
@@ -16,7 +18,10 @@ import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
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 uy.kohesive.injekt.injectLazy
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.CompletableFuture
|
||||
@@ -90,7 +95,11 @@ class MangaMutation {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateManga(input: UpdateMangaInput): CompletableFuture<DataFetcherResult<UpdateMangaPayload?>> {
|
||||
fun updateManga(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateMangaInput,
|
||||
): CompletableFuture<DataFetcherResult<UpdateMangaPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
return future {
|
||||
@@ -110,7 +119,11 @@ class MangaMutation {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMangas(input: UpdateMangasInput): CompletableFuture<DataFetcherResult<UpdateMangasPayload?>> {
|
||||
fun updateMangas(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateMangasInput,
|
||||
): CompletableFuture<DataFetcherResult<UpdateMangasPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
return future {
|
||||
@@ -140,7 +153,11 @@ class MangaMutation {
|
||||
val manga: MangaType,
|
||||
)
|
||||
|
||||
fun fetchManga(input: FetchMangaInput): CompletableFuture<DataFetcherResult<FetchMangaPayload?>> {
|
||||
fun fetchManga(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: FetchMangaInput,
|
||||
): CompletableFuture<DataFetcherResult<FetchMangaPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, id) = input
|
||||
|
||||
return future {
|
||||
@@ -169,7 +186,11 @@ class MangaMutation {
|
||||
val meta: MangaMetaType,
|
||||
)
|
||||
|
||||
fun setMangaMeta(input: SetMangaMetaInput): DataFetcherResult<SetMangaMetaPayload?> {
|
||||
fun setMangaMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: SetMangaMetaInput,
|
||||
): DataFetcherResult<SetMangaMetaPayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
@@ -191,7 +212,11 @@ class MangaMutation {
|
||||
val manga: MangaType,
|
||||
)
|
||||
|
||||
fun deleteMangaMeta(input: DeleteMangaMetaInput): DataFetcherResult<DeleteMangaMetaPayload?> {
|
||||
fun deleteMangaMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DeleteMangaMetaInput,
|
||||
): DataFetcherResult<DeleteMangaMetaPayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, mangaId, key) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
@@ -8,7 +9,11 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||
import suwayomi.tachidesk.global.model.table.GlobalMetaTable
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.GlobalMetaType
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
|
||||
class MetaMutation {
|
||||
data class SetGlobalMetaInput(
|
||||
@@ -21,7 +26,11 @@ class MetaMutation {
|
||||
val meta: GlobalMetaType,
|
||||
)
|
||||
|
||||
fun setGlobalMeta(input: SetGlobalMetaInput): DataFetcherResult<SetGlobalMetaPayload?> {
|
||||
fun setGlobalMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: SetGlobalMetaInput,
|
||||
): DataFetcherResult<SetGlobalMetaPayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
@@ -41,7 +50,11 @@ class MetaMutation {
|
||||
val meta: GlobalMetaType?,
|
||||
)
|
||||
|
||||
fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DataFetcherResult<DeleteGlobalMetaPayload?> {
|
||||
fun deleteGlobalMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DeleteGlobalMetaInput,
|
||||
): DataFetcherResult<DeleteGlobalMetaPayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, key) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.PartialSettingsType
|
||||
import suwayomi.tachidesk.graphql.types.Settings
|
||||
import suwayomi.tachidesk.graphql.types.SettingsType
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.repoMatchRegex
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME
|
||||
import suwayomi.tachidesk.server.ServerConfig
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import java.io.File
|
||||
|
||||
@@ -226,7 +231,11 @@ class SettingsMutation {
|
||||
updateSetting(settings.koreaderSyncPercentageTolerance, serverConfig.koreaderSyncPercentageTolerance)
|
||||
}
|
||||
|
||||
fun setSettings(input: SetSettingsInput): SetSettingsPayload {
|
||||
fun setSettings(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: SetSettingsInput,
|
||||
): SetSettingsPayload {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, settings) = input
|
||||
|
||||
validateSettings(settings)
|
||||
@@ -244,7 +253,11 @@ class SettingsMutation {
|
||||
val settings: SettingsType,
|
||||
)
|
||||
|
||||
fun resetSettings(input: ResetSettingsInput): ResetSettingsPayload {
|
||||
fun resetSettings(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: ResetSettingsInput,
|
||||
): ResetSettingsPayload {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId) = input
|
||||
|
||||
GlobalConfigManager.resetUserConfig()
|
||||
|
||||
@@ -6,12 +6,14 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.FilterChange
|
||||
import suwayomi.tachidesk.graphql.types.MangaType
|
||||
import suwayomi.tachidesk.graphql.types.Preference
|
||||
@@ -25,7 +27,10 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
|
||||
class SourceMutation {
|
||||
@@ -39,7 +44,11 @@ class SourceMutation {
|
||||
val meta: SourceMetaType,
|
||||
)
|
||||
|
||||
fun setSourceMeta(input: SetSourceMetaInput): DataFetcherResult<SetSourceMetaPayload?> {
|
||||
fun setSourceMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: SetSourceMetaInput,
|
||||
): DataFetcherResult<SetSourceMetaPayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, meta) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
@@ -61,7 +70,11 @@ class SourceMutation {
|
||||
val source: SourceType?,
|
||||
)
|
||||
|
||||
fun deleteSourceMeta(input: DeleteSourceMetaInput): DataFetcherResult<DeleteSourceMetaPayload?> {
|
||||
fun deleteSourceMeta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DeleteSourceMetaInput,
|
||||
): DataFetcherResult<DeleteSourceMetaPayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, sourceId, key) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
@@ -116,7 +129,11 @@ class SourceMutation {
|
||||
val hasNextPage: Boolean,
|
||||
)
|
||||
|
||||
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
|
||||
fun fetchSourceManga(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: FetchSourceMangaInput,
|
||||
): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, sourceId, type, page, query, filters) = input
|
||||
|
||||
return future {
|
||||
@@ -182,7 +199,11 @@ class SourceMutation {
|
||||
val source: SourceType,
|
||||
)
|
||||
|
||||
fun updateSourcePreference(input: UpdateSourcePreferenceInput): DataFetcherResult<UpdateSourcePreferencePayload?> {
|
||||
fun updateSourcePreference(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateSourcePreferenceInput,
|
||||
): DataFetcherResult<UpdateSourcePreferencePayload?> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, sourceId, change) = input
|
||||
|
||||
return asDataFetcherResult {
|
||||
|
||||
@@ -3,16 +3,21 @@ package suwayomi.tachidesk.graphql.mutations
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
||||
import suwayomi.tachidesk.graphql.types.TrackerType
|
||||
import suwayomi.tachidesk.manga.impl.track.Track
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
|
||||
class TrackMutation {
|
||||
@@ -28,7 +33,11 @@ class TrackMutation {
|
||||
val tracker: TrackerType,
|
||||
)
|
||||
|
||||
fun loginTrackerOAuth(input: LoginTrackerOAuthInput): CompletableFuture<LoginTrackerOAuthPayload> {
|
||||
fun loginTrackerOAuth(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: LoginTrackerOAuthInput,
|
||||
): CompletableFuture<LoginTrackerOAuthPayload> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val tracker =
|
||||
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
||||
"Could not find tracker"
|
||||
@@ -57,7 +66,11 @@ class TrackMutation {
|
||||
val tracker: TrackerType,
|
||||
)
|
||||
|
||||
fun loginTrackerCredentials(input: LoginTrackerCredentialsInput): CompletableFuture<LoginTrackerCredentialsPayload> {
|
||||
fun loginTrackerCredentials(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: LoginTrackerCredentialsInput,
|
||||
): CompletableFuture<LoginTrackerCredentialsPayload> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val tracker =
|
||||
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
||||
"Could not find tracker"
|
||||
@@ -84,7 +97,11 @@ class TrackMutation {
|
||||
val tracker: TrackerType,
|
||||
)
|
||||
|
||||
fun logoutTracker(input: LogoutTrackerInput): CompletableFuture<LogoutTrackerPayload> {
|
||||
fun logoutTracker(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: LogoutTrackerInput,
|
||||
): CompletableFuture<LogoutTrackerPayload> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val tracker =
|
||||
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
||||
"Could not find tracker"
|
||||
@@ -117,7 +134,11 @@ class TrackMutation {
|
||||
val trackRecord: TrackRecordType,
|
||||
)
|
||||
|
||||
fun bindTrack(input: BindTrackInput): CompletableFuture<BindTrackPayload> {
|
||||
fun bindTrack(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: BindTrackInput,
|
||||
): CompletableFuture<BindTrackPayload> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, mangaId, trackerId, remoteId, private) = input
|
||||
|
||||
return future {
|
||||
@@ -152,7 +173,11 @@ class TrackMutation {
|
||||
val trackRecord: TrackRecordType,
|
||||
)
|
||||
|
||||
fun fetchTrack(input: FetchTrackInput): CompletableFuture<FetchTrackPayload> {
|
||||
fun fetchTrack(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: FetchTrackInput,
|
||||
): CompletableFuture<FetchTrackPayload> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, recordId) = input
|
||||
|
||||
return future {
|
||||
@@ -184,7 +209,11 @@ class TrackMutation {
|
||||
val trackRecord: TrackRecordType?,
|
||||
)
|
||||
|
||||
fun unbindTrack(input: UnbindTrackInput): CompletableFuture<UnbindTrackPayload> {
|
||||
fun unbindTrack(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UnbindTrackInput,
|
||||
): CompletableFuture<UnbindTrackPayload> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, recordId, deleteRemoteTrack) = input
|
||||
|
||||
return future {
|
||||
@@ -214,7 +243,11 @@ class TrackMutation {
|
||||
val trackRecords: List<TrackRecordType>,
|
||||
)
|
||||
|
||||
fun trackProgress(input: TrackProgressInput): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
|
||||
fun trackProgress(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: TrackProgressInput,
|
||||
): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (clientMutationId, mangaId) = input
|
||||
|
||||
return future {
|
||||
@@ -256,8 +289,12 @@ class TrackMutation {
|
||||
val trackRecord: TrackRecordType?,
|
||||
)
|
||||
|
||||
fun updateTrack(input: UpdateTrackInput): CompletableFuture<UpdateTrackPayload> =
|
||||
fun updateTrack(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateTrackInput,
|
||||
): CompletableFuture<UpdateTrackPayload> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Track.update(
|
||||
Track.UpdateInput(
|
||||
input.recordId,
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.execution.DataFetcherResult
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import suwayomi.tachidesk.graphql.asDataFetcherResult
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus
|
||||
import suwayomi.tachidesk.graphql.types.UpdateStatus
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||
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 uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -26,7 +31,11 @@ class UpdateMutation {
|
||||
val updateStatus: LibraryUpdateStatus,
|
||||
)
|
||||
|
||||
fun updateLibrary(input: UpdateLibraryInput): CompletableFuture<DataFetcherResult<UpdateLibraryPayload?>> {
|
||||
fun updateLibrary(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateLibraryInput,
|
||||
): CompletableFuture<DataFetcherResult<UpdateLibraryPayload?>> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
updater.addCategoriesToUpdateQueue(
|
||||
Category.getCategoryList().filter { input.categories?.contains(it.id) ?: true },
|
||||
clear = true,
|
||||
@@ -57,8 +66,12 @@ class UpdateMutation {
|
||||
val updateStatus: UpdateStatus,
|
||||
)
|
||||
|
||||
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<DataFetcherResult<UpdateLibraryMangaPayload?>> {
|
||||
fun updateLibraryManga(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateLibraryMangaInput,
|
||||
): CompletableFuture<DataFetcherResult<UpdateLibraryMangaPayload?>> {
|
||||
updateLibrary(
|
||||
dataFetchingEnvironment,
|
||||
UpdateLibraryInput(
|
||||
clientMutationId = input.clientMutationId,
|
||||
categories = null,
|
||||
@@ -88,8 +101,12 @@ class UpdateMutation {
|
||||
val updateStatus: UpdateStatus,
|
||||
)
|
||||
|
||||
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<DataFetcherResult<UpdateCategoryMangaPayload?>> {
|
||||
fun updateCategoryManga(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateCategoryMangaInput,
|
||||
): CompletableFuture<DataFetcherResult<UpdateCategoryMangaPayload?>> {
|
||||
updateLibrary(
|
||||
dataFetchingEnvironment,
|
||||
UpdateLibraryInput(
|
||||
clientMutationId = input.clientMutationId,
|
||||
categories = input.categories,
|
||||
@@ -117,7 +134,11 @@ class UpdateMutation {
|
||||
val clientMutationId: String?,
|
||||
)
|
||||
|
||||
fun updateStop(input: UpdateStopInput): UpdateStopPayload {
|
||||
fun updateStop(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: UpdateStopInput,
|
||||
): UpdateStopPayload {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
updater.reset()
|
||||
return UpdateStopPayload(input.clientMutationId)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package suwayomi.tachidesk.graphql.mutations
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import suwayomi.tachidesk.global.impl.util.Jwt
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.server.user.UserType
|
||||
|
||||
class UserMutation {
|
||||
data class LoginInput(
|
||||
val clientMutationId: String? = null,
|
||||
val username: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
data class LoginPayload(
|
||||
val clientMutationId: String?,
|
||||
val accessToken: String,
|
||||
val refreshToken: String,
|
||||
)
|
||||
|
||||
fun login(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: LoginInput,
|
||||
): LoginPayload {
|
||||
if (dataFetchingEnvironment.getAttribute(Attribute.TachideskUser) !is UserType.Visitor) {
|
||||
throw IllegalArgumentException("Cannot login while already logged-in")
|
||||
}
|
||||
val isValid =
|
||||
input.username == serverConfig.authUsername.value &&
|
||||
input.password == serverConfig.authPassword.value
|
||||
if (isValid) {
|
||||
val jwt = Jwt.generateJwt()
|
||||
return LoginPayload(
|
||||
clientMutationId = input.clientMutationId,
|
||||
accessToken = jwt.accessToken,
|
||||
refreshToken = jwt.refreshToken,
|
||||
)
|
||||
} else {
|
||||
throw Exception("Incorrect username or password.")
|
||||
}
|
||||
}
|
||||
|
||||
data class RefreshTokenInput(
|
||||
val clientMutationId: String? = null,
|
||||
val refreshToken: String,
|
||||
)
|
||||
|
||||
data class RefreshTokenPayload(
|
||||
val clientMutationId: String?,
|
||||
val accessToken: String,
|
||||
)
|
||||
|
||||
fun refreshToken(input: RefreshTokenInput): RefreshTokenPayload {
|
||||
val accessToken = Jwt.refreshJwt(input.refreshToken)
|
||||
|
||||
return RefreshTokenPayload(
|
||||
clientMutationId = input.clientMutationId,
|
||||
accessToken = accessToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package suwayomi.tachidesk.graphql.queries
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import io.javalin.http.UploadedFile
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.BackupRestoreStatus
|
||||
import suwayomi.tachidesk.graphql.types.toStatus
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
|
||||
class BackupQuery {
|
||||
data class ValidateBackupInput(
|
||||
@@ -25,7 +30,11 @@ class BackupQuery {
|
||||
val missingTrackers: List<ValidateBackupTracker>,
|
||||
)
|
||||
|
||||
fun validateBackup(input: ValidateBackupInput): ValidateBackupResult {
|
||||
fun validateBackup(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: ValidateBackupInput,
|
||||
): ValidateBackupResult {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val result = ProtoBackupValidator.validate(input.backup.content())
|
||||
return ValidateBackupResult(
|
||||
result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) },
|
||||
@@ -33,5 +42,11 @@ class BackupQuery {
|
||||
)
|
||||
}
|
||||
|
||||
fun restoreStatus(id: String): BackupRestoreStatus? = ProtoBackupImport.getRestoreState(id)?.toStatus()
|
||||
fun restoreStatus(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
id: String,
|
||||
): BackupRestoreStatus? {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return ProtoBackupImport.getRestoreState(id)?.toStatus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
@@ -39,13 +40,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.CategoryNodeList
|
||||
import suwayomi.tachidesk.graphql.types.CategoryType
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class CategoryQuery {
|
||||
fun category(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
id: Int,
|
||||
): CompletableFuture<CategoryType> = dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id)
|
||||
): CompletableFuture<CategoryType> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id)
|
||||
}
|
||||
|
||||
enum class CategoryOrderBy(
|
||||
override val column: Column<*>,
|
||||
@@ -121,6 +128,7 @@ class CategoryQuery {
|
||||
}
|
||||
|
||||
fun categories(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
condition: CategoryCondition? = null,
|
||||
filter: CategoryFilter? = null,
|
||||
@GraphQLDeprecated(
|
||||
@@ -140,6 +148,7 @@ class CategoryQuery {
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): CategoryNodeList {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res = CategoryTable.selectAll()
|
||||
|
||||
@@ -30,6 +30,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
@@ -43,6 +44,9 @@ import suwayomi.tachidesk.graphql.types.ChapterNodeList
|
||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
@@ -197,6 +201,7 @@ class ChapterQuery {
|
||||
}
|
||||
|
||||
fun chapters(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
condition: ChapterCondition? = null,
|
||||
filter: ChapterFilter? = null,
|
||||
@GraphQLDeprecated(
|
||||
@@ -216,6 +221,7 @@ class ChapterQuery {
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): ChapterNodeList {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res = ChapterTable.selectAll()
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package suwayomi.tachidesk.graphql.queries
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.DownloadStatus
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
|
||||
class DownloadQuery {
|
||||
fun downloadStatus(): CompletableFuture<DownloadStatus> =
|
||||
fun downloadStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<DownloadStatus> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
DownloadStatus(DownloadManager.getStatus())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
@@ -40,13 +41,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionNodeList
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionType
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class ExtensionQuery {
|
||||
fun extension(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
pkgName: String,
|
||||
): CompletableFuture<ExtensionType> = dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName)
|
||||
): CompletableFuture<ExtensionType> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName)
|
||||
}
|
||||
|
||||
enum class ExtensionOrderBy(
|
||||
override val column: Column<*>,
|
||||
@@ -153,6 +160,7 @@ class ExtensionQuery {
|
||||
}
|
||||
|
||||
fun extensions(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
condition: ExtensionCondition? = null,
|
||||
filter: ExtensionFilter? = null,
|
||||
@GraphQLDeprecated(
|
||||
@@ -172,6 +180,7 @@ class ExtensionQuery {
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): ExtensionNodeList {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res = ExtensionTable.selectAll()
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package suwayomi.tachidesk.graphql.queries
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import suwayomi.tachidesk.global.impl.AppUpdate
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.AboutWebUI
|
||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
||||
import suwayomi.tachidesk.graphql.types.WebUIUpdateCheck
|
||||
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import suwayomi.tachidesk.server.util.WebInterfaceManager
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -42,8 +47,9 @@ class InfoQuery {
|
||||
val url: String,
|
||||
)
|
||||
|
||||
fun checkForServerUpdates(): CompletableFuture<List<CheckForServerUpdatesPayload>> =
|
||||
fun checkForServerUpdates(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<CheckForServerUpdatesPayload>> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
AppUpdate.checkUpdate().map {
|
||||
CheckForServerUpdatesPayload(
|
||||
channel = it.channel,
|
||||
@@ -53,13 +59,15 @@ class InfoQuery {
|
||||
}
|
||||
}
|
||||
|
||||
fun aboutWebUI(): CompletableFuture<AboutWebUI> =
|
||||
fun aboutWebUI(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<AboutWebUI> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
WebInterfaceManager.getAboutInfo()
|
||||
}
|
||||
|
||||
fun checkForWebUIUpdate(): CompletableFuture<WebUIUpdateCheck> =
|
||||
fun checkForWebUIUpdate(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<WebUIUpdateCheck> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(WebUIFlavor.current, raiseError = true)
|
||||
WebUIUpdateCheck(
|
||||
channel = serverConfig.webUIChannel.value,
|
||||
@@ -68,5 +76,8 @@ class InfoQuery {
|
||||
)
|
||||
}
|
||||
|
||||
fun getWebUIUpdateStatus(): WebUIUpdateStatus = WebInterfaceManager.status.value
|
||||
fun getWebUIUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): WebUIUpdateStatus {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return WebInterfaceManager.status.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package suwayomi.tachidesk.graphql.queries
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.KoSyncStatusPayload
|
||||
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
|
||||
class KoreaderSyncQuery {
|
||||
fun koSyncStatus(): CompletableFuture<KoSyncStatusPayload> =
|
||||
fun koSyncStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<KoSyncStatusPayload> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
KoreaderSyncService.getStatus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
@@ -42,13 +43,19 @@ import suwayomi.tachidesk.graphql.types.MangaType
|
||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class MangaQuery {
|
||||
fun manga(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
id: Int,
|
||||
): CompletableFuture<MangaType> = dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id)
|
||||
): CompletableFuture<MangaType> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id)
|
||||
}
|
||||
|
||||
enum class MangaOrderBy(
|
||||
override val column: Column<*>,
|
||||
@@ -216,6 +223,7 @@ class MangaQuery {
|
||||
}
|
||||
|
||||
fun mangas(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
condition: MangaCondition? = null,
|
||||
filter: MangaFilter? = null,
|
||||
@GraphQLDeprecated(
|
||||
@@ -235,6 +243,7 @@ class MangaQuery {
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): MangaNodeList {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res =
|
||||
|
||||
@@ -24,6 +24,7 @@ import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
@@ -35,13 +36,19 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.GlobalMetaNodeList
|
||||
import suwayomi.tachidesk.graphql.types.GlobalMetaType
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class MetaQuery {
|
||||
fun meta(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
key: String,
|
||||
): CompletableFuture<GlobalMetaType> = dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key)
|
||||
): CompletableFuture<GlobalMetaType> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key)
|
||||
}
|
||||
|
||||
enum class MetaOrderBy(
|
||||
override val column: Column<*>,
|
||||
@@ -105,6 +112,7 @@ class MetaQuery {
|
||||
}
|
||||
|
||||
fun metas(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
condition: MetaCondition? = null,
|
||||
filter: MetaFilter? = null,
|
||||
@GraphQLDeprecated(
|
||||
@@ -124,6 +132,7 @@ class MetaQuery {
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): GlobalMetaNodeList {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res = GlobalMetaTable.selectAll()
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
package suwayomi.tachidesk.graphql.queries
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.SettingsType
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
|
||||
class SettingsQuery {
|
||||
fun settings(): SettingsType = SettingsType()
|
||||
fun settings(dataFetchingEnvironment: DataFetchingEnvironment): SettingsType {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return SettingsType()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
@@ -39,13 +40,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.SourceNodeList
|
||||
import suwayomi.tachidesk.graphql.types.SourceType
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class SourceQuery {
|
||||
fun source(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
id: Long,
|
||||
): CompletableFuture<SourceType> = dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id)
|
||||
): CompletableFuture<SourceType> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id)
|
||||
}
|
||||
|
||||
enum class SourceOrderBy(
|
||||
override val column: Column<*>,
|
||||
@@ -121,6 +128,7 @@ class SourceQuery {
|
||||
}
|
||||
|
||||
fun sources(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
condition: SourceCondition? = null,
|
||||
filter: SourceFilter? = null,
|
||||
@GraphQLDeprecated(
|
||||
@@ -140,6 +148,7 @@ class SourceQuery {
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): SourceNodeList {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (queryResults, resultsAsType) =
|
||||
transaction {
|
||||
val res = SourceTable.selectAll()
|
||||
|
||||
@@ -22,6 +22,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
@@ -39,14 +40,20 @@ import suwayomi.tachidesk.graphql.types.TrackerType
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||
import suwayomi.tachidesk.manga.model.table.insertAll
|
||||
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 java.util.concurrent.CompletableFuture
|
||||
|
||||
class TrackQuery {
|
||||
fun tracker(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
id: Int,
|
||||
): CompletableFuture<TrackerType> = dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", id)
|
||||
): CompletableFuture<TrackerType> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", id)
|
||||
}
|
||||
|
||||
enum class TrackerOrderBy {
|
||||
ID,
|
||||
@@ -115,6 +122,7 @@ class TrackQuery {
|
||||
)
|
||||
|
||||
fun trackers(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
condition: TrackerCondition? = null,
|
||||
@GraphQLDeprecated(
|
||||
"Replaced with order",
|
||||
@@ -133,6 +141,7 @@ class TrackQuery {
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): TrackerNodeList {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val (queryResults, resultsAsType) =
|
||||
run {
|
||||
var res = TrackerManager.services.map { TrackerType(it) }
|
||||
@@ -240,8 +249,10 @@ class TrackQuery {
|
||||
fun trackRecord(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
id: Int,
|
||||
): CompletableFuture<TrackRecordType> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordType>("TrackRecordDataLoader", id)
|
||||
): CompletableFuture<TrackRecordType> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordType>("TrackRecordDataLoader", id)
|
||||
}
|
||||
|
||||
enum class TrackRecordOrderBy(
|
||||
override val column: Column<*>,
|
||||
@@ -389,6 +400,7 @@ class TrackQuery {
|
||||
}
|
||||
|
||||
fun trackRecords(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
condition: TrackRecordCondition? = null,
|
||||
filter: TrackRecordFilter? = null,
|
||||
@GraphQLDeprecated(
|
||||
@@ -408,6 +420,7 @@ class TrackQuery {
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): TrackRecordNodeList {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res = TrackRecordTable.selectAll()
|
||||
@@ -490,8 +503,12 @@ class TrackQuery {
|
||||
val trackSearches: List<TrackSearchType>,
|
||||
)
|
||||
|
||||
fun searchTracker(input: SearchTrackerInput): CompletableFuture<SearchTrackerPayload> =
|
||||
fun searchTracker(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: SearchTrackerInput,
|
||||
): CompletableFuture<SearchTrackerPayload> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val tracker =
|
||||
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
||||
"Tracker not found"
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package suwayomi.tachidesk.graphql.queries
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.flow.first
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus
|
||||
import suwayomi.tachidesk.graphql.types.UpdateStatus
|
||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||
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 uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -13,13 +18,24 @@ class UpdateQuery {
|
||||
private val updater: IUpdater by injectLazy()
|
||||
|
||||
@GraphQLDeprecated("Replaced with libraryUpdateStatus", ReplaceWith("libraryUpdateStatus"))
|
||||
fun updateStatus(): CompletableFuture<UpdateStatus> = future { UpdateStatus(updater.status.first()) }
|
||||
fun updateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<UpdateStatus> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
UpdateStatus(updater.status.first())
|
||||
}
|
||||
|
||||
fun libraryUpdateStatus(): CompletableFuture<LibraryUpdateStatus> = future { LibraryUpdateStatus(updater.getStatus()) }
|
||||
fun libraryUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<LibraryUpdateStatus> =
|
||||
future {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
LibraryUpdateStatus(updater.getStatus())
|
||||
}
|
||||
|
||||
data class LastUpdateTimestampPayload(
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
||||
fun lastUpdateTimestamp(): LastUpdateTimestampPayload = LastUpdateTimestampPayload(updater.getLastUpdateTimestamp())
|
||||
fun lastUpdateTimestamp(dataFetchingEnvironment: DataFetchingEnvironment): LastUpdateTimestampPayload {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return LastUpdateTimestampPayload(updater.getLastUpdateTimestamp())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,34 +9,49 @@ package suwayomi.tachidesk.graphql.server
|
||||
|
||||
import com.expediagroup.graphql.server.execution.GraphQLContextFactory
|
||||
import graphql.GraphQLContext
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import io.javalin.http.Context
|
||||
import io.javalin.websocket.WsContext
|
||||
import org.dataloader.BatchLoaderEnvironment
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.UserType
|
||||
|
||||
/**
|
||||
* Custom logic for how Suwayomi-Server should create its context given the [Context]
|
||||
*/
|
||||
class TachideskGraphQLContextFactory : GraphQLContextFactory<Context> {
|
||||
override suspend fun generateContext(request: Context): GraphQLContext = emptyMap<Any, Any>().toGraphQLContext()
|
||||
// mutableMapOf<Any, Any>(
|
||||
// "user" to User(
|
||||
// email = "fake@site.com",
|
||||
// firstName = "Someone",
|
||||
// lastName = "You Don't know",
|
||||
// universityId = 4
|
||||
// )
|
||||
// ).also { map ->
|
||||
// request.headers["my-custom-header"]?.let { customHeader ->
|
||||
// map["customHeader"] = customHeader
|
||||
// }
|
||||
// }.toGraphQLContext()
|
||||
override suspend fun generateContext(request: Context): GraphQLContext =
|
||||
mapOf(
|
||||
Context::class to request,
|
||||
request.getPair(Attribute.TachideskUser),
|
||||
).toGraphQLContext()
|
||||
|
||||
fun generateContextMap(
|
||||
@Suppress("UNUSED_PARAMETER") request: WsContext,
|
||||
): Map<*, Any> = emptyMap<Any, Any>()
|
||||
user: UserType,
|
||||
request: WsContext,
|
||||
): Map<*, Any> =
|
||||
mapOf(
|
||||
Context::class to request,
|
||||
Attribute.TachideskUser to user,
|
||||
)
|
||||
|
||||
private fun <T : Any> Context.getPair(attribute: Attribute<T>) = attribute to getAttribute(attribute)
|
||||
|
||||
private fun <T : Any> WsContext.getPair(attribute: Attribute<T>) = attribute to getAttribute(attribute)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [GraphQLContext] from [this] map
|
||||
* @return a new [GraphQLContext]
|
||||
*/
|
||||
fun Map<*, Any?>.toGraphQLContext(): graphql.GraphQLContext = graphql.GraphQLContext.of(this)
|
||||
fun Map<*, Any?>.toGraphQLContext(): GraphQLContext = GraphQLContext.of(this)
|
||||
|
||||
fun <T : Any> GraphQLContext.getAttribute(attribute: Attribute<T>): T = get(attribute)
|
||||
|
||||
fun <T : Any> DataFetchingEnvironment.getAttribute(attribute: Attribute<T>): T = graphQlContext.get(attribute)
|
||||
|
||||
val BatchLoaderEnvironment.graphQlContext: GraphQLContext
|
||||
get() = keyContextsList.filterIsInstance<GraphQLContext>().first()
|
||||
|
||||
fun <T : Any> BatchLoaderEnvironment.getAttribute(attribute: Attribute<T>): T = graphQlContext.getAttribute(attribute)
|
||||
|
||||
@@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.mutations.SettingsMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.SourceMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.TrackMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.UpdateMutation
|
||||
import suwayomi.tachidesk.graphql.mutations.UserMutation
|
||||
import suwayomi.tachidesk.graphql.queries.BackupQuery
|
||||
import suwayomi.tachidesk.graphql.queries.CategoryQuery
|
||||
import suwayomi.tachidesk.graphql.queries.ChapterQuery
|
||||
@@ -100,6 +101,7 @@ val schema =
|
||||
TopLevelObject(SourceMutation()),
|
||||
TopLevelObject(TrackMutation()),
|
||||
TopLevelObject(UpdateMutation()),
|
||||
TopLevelObject(UserMutation()),
|
||||
),
|
||||
subscriptions =
|
||||
listOf(
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.http.Header
|
||||
import io.javalin.websocket.WsContext
|
||||
import io.javalin.websocket.WsMessageContext
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
@@ -36,6 +37,8 @@ import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMess
|
||||
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_ERROR
|
||||
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_NEXT
|
||||
import suwayomi.tachidesk.graphql.server.toGraphQLContext
|
||||
import suwayomi.tachidesk.server.user.UserType
|
||||
import suwayomi.tachidesk.server.user.getUserFromToken
|
||||
|
||||
/**
|
||||
* Implementation of the `graphql-transport-ws` protocol defined by Denis Badurina
|
||||
@@ -77,7 +80,7 @@ class ApolloSubscriptionProtocolHandler(
|
||||
|
||||
return try {
|
||||
when (operationMessage.type) {
|
||||
GQL_CONNECTION_INIT.type -> onInit(context)
|
||||
GQL_CONNECTION_INIT.type -> onInit(operationMessage, context)
|
||||
GQL_SUBSCRIBE.type -> startSubscription(operationMessage, context)
|
||||
GQL_COMPLETE.type -> onComplete(operationMessage)
|
||||
GQL_PING.type -> onPing()
|
||||
@@ -144,17 +147,27 @@ class ApolloSubscriptionProtocolHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun onInit(context: WsContext): Flow<SubscriptionOperationMessage> {
|
||||
saveContext(context)
|
||||
private fun onInit(
|
||||
operationMessage: SubscriptionOperationMessage,
|
||||
context: WsContext,
|
||||
): Flow<SubscriptionOperationMessage> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val payload = operationMessage.payload as? Map<String, Any?>
|
||||
val token = payload?.let { it[Header.AUTHORIZATION] as? String }
|
||||
|
||||
saveContext(getUserFromToken(token), context)
|
||||
return flowOf(acknowledgeMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the context and save it for all future messages.
|
||||
*/
|
||||
private fun saveContext(context: WsContext) {
|
||||
private fun saveContext(
|
||||
user: UserType,
|
||||
context: WsContext,
|
||||
) {
|
||||
runBlocking {
|
||||
val graphQLContext = contextFactory.generateContextMap(context).toGraphQLContext()
|
||||
val graphQLContext = contextFactory.generateContextMap(user, context).toGraphQLContext()
|
||||
sessionState.saveContext(context, graphQLContext)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,18 +9,25 @@ package suwayomi.tachidesk.graphql.subscriptions
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.DownloadStatus
|
||||
import suwayomi.tachidesk.graphql.types.DownloadUpdates
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
|
||||
class DownloadSubscription {
|
||||
@GraphQLDeprecated("Replaced with downloadStatusChanged", ReplaceWith("downloadStatusChanged(input)"))
|
||||
fun downloadChanged(): Flow<DownloadStatus> =
|
||||
DownloadManager.status.map { downloadStatus ->
|
||||
fun downloadChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flow<DownloadStatus> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return DownloadManager.status.map { downloadStatus ->
|
||||
DownloadStatus(downloadStatus)
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadChangedInput(
|
||||
@GraphQLDescription(
|
||||
@@ -33,7 +40,11 @@ class DownloadSubscription {
|
||||
val maxUpdates: Int?,
|
||||
)
|
||||
|
||||
fun downloadStatusChanged(input: DownloadChangedInput): Flow<DownloadUpdates> {
|
||||
fun downloadStatusChanged(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: DownloadChangedInput,
|
||||
): Flow<DownloadUpdates> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val omitUpdates = input.maxUpdates != null
|
||||
val maxUpdates = input.maxUpdates ?: 50
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
package suwayomi.tachidesk.graphql.subscriptions
|
||||
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import suwayomi.tachidesk.server.util.WebInterfaceManager
|
||||
|
||||
class InfoSubscription {
|
||||
fun webUIUpdateStatusChange(): Flow<WebUIUpdateStatus> = WebInterfaceManager.status
|
||||
fun webUIUpdateStatusChange(dataFetchingEnvironment: DataFetchingEnvironment): Flow<WebUIUpdateStatus> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return WebInterfaceManager.status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,22 +9,29 @@ package suwayomi.tachidesk.graphql.subscriptions
|
||||
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||
import suwayomi.tachidesk.graphql.types.UpdateStatus
|
||||
import suwayomi.tachidesk.graphql.types.UpdaterUpdates
|
||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||
import suwayomi.tachidesk.manga.impl.update.UpdateUpdates
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class UpdateSubscription {
|
||||
private val updater: IUpdater by injectLazy()
|
||||
|
||||
@GraphQLDeprecated("Replaced with updates", ReplaceWith("updates(input)"))
|
||||
fun updateStatusChanged(): Flow<UpdateStatus> =
|
||||
updater.status.map { updateStatus ->
|
||||
fun updateStatusChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flow<UpdateStatus> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
return updater.status.map { updateStatus ->
|
||||
UpdateStatus(updateStatus)
|
||||
}
|
||||
}
|
||||
|
||||
data class LibraryUpdateStatusChangedInput(
|
||||
@GraphQLDescription(
|
||||
@@ -37,7 +44,11 @@ class UpdateSubscription {
|
||||
val maxUpdates: Int?,
|
||||
)
|
||||
|
||||
fun libraryUpdateStatusChanged(input: LibraryUpdateStatusChangedInput): Flow<UpdaterUpdates> {
|
||||
fun libraryUpdateStatusChanged(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
input: LibraryUpdateStatusChangedInput,
|
||||
): Flow<UpdaterUpdates> {
|
||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val omitUpdates = input.maxUpdates != null
|
||||
val maxUpdates = input.maxUpdates ?: 50
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ enum class AuthMode {
|
||||
NONE,
|
||||
BASIC_AUTH,
|
||||
SIMPLE_LOGIN,
|
||||
UI_LOGIN,
|
||||
// TODO: ACCOUNT for #623
|
||||
;
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||
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.withOperation
|
||||
|
||||
@@ -28,6 +31,7 @@ object BackupController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
ProtoBackupImport.restoreLegacy(ctx.bodyInputStream())
|
||||
@@ -55,6 +59,7 @@ object BackupController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -80,6 +85,7 @@ object BackupController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.contentType("application/octet-stream")
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -112,6 +118,7 @@ object BackupController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.contentType("application/octet-stream")
|
||||
|
||||
ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""")
|
||||
@@ -146,6 +153,7 @@ object BackupController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
ProtoBackupValidator.validate(ctx.bodyInputStream())
|
||||
@@ -177,6 +185,7 @@ object BackupController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content())
|
||||
|
||||
@@ -12,6 +12,9 @@ import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
||||
import suwayomi.tachidesk.server.user.requireUser
|
||||
import suwayomi.tachidesk.server.util.formParam
|
||||
import suwayomi.tachidesk.server.util.handler
|
||||
import suwayomi.tachidesk.server.util.pathParam
|
||||
@@ -28,6 +31,7 @@ object CategoryController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(Category.getCategoryList())
|
||||
},
|
||||
withResults = {
|
||||
@@ -46,6 +50,7 @@ object CategoryController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, name ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
if (Category.createCategory(name) != -1) {
|
||||
ctx.status(200)
|
||||
} else {
|
||||
@@ -73,6 +78,7 @@ object CategoryController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate, includeInDownload ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Category.updateCategory(categoryId, name, isDefault, includeInUpdate, includeInDownload)
|
||||
ctx.status(200)
|
||||
},
|
||||
@@ -92,6 +98,7 @@ object CategoryController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Category.removeCategory(categoryId)
|
||||
ctx.status(200)
|
||||
},
|
||||
@@ -111,6 +118,7 @@ object CategoryController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(CategoryManga.getCategoryMangaList(categoryId))
|
||||
},
|
||||
withResults = {
|
||||
@@ -130,6 +138,7 @@ object CategoryController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, from, to ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Category.reorderCategory(from, to)
|
||||
ctx.status(200)
|
||||
},
|
||||
@@ -151,6 +160,7 @@ object CategoryController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId, key, value ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Category.modifyMeta(categoryId, key, value)
|
||||
ctx.status(200)
|
||||
},
|
||||
|
||||
@@ -12,7 +12,10 @@ import io.javalin.websocket.WsConfig
|
||||
import kotlinx.serialization.json.Json
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
||||
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
|
||||
@@ -24,6 +27,7 @@ object DownloadController {
|
||||
/** Download queue stats */
|
||||
fun downloadsWS(ws: WsConfig) {
|
||||
ws.onConnect { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
DownloadManager.addClient(ctx)
|
||||
DownloadManager.notifyClient(ctx)
|
||||
}
|
||||
@@ -44,7 +48,8 @@ object DownloadController {
|
||||
description("Start the downloader")
|
||||
}
|
||||
},
|
||||
behaviorOf = {
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
DownloadManager.start()
|
||||
},
|
||||
withResults = {
|
||||
@@ -62,6 +67,7 @@ object DownloadController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { DownloadManager.stop() }
|
||||
.thenApply { ctx.status(HttpStatus.OK) }
|
||||
@@ -82,6 +88,7 @@ object DownloadController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { DownloadManager.clear() }
|
||||
.thenApply { ctx.status(HttpStatus.OK) }
|
||||
@@ -104,6 +111,7 @@ object DownloadController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, chapterIndex, mangaId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex)
|
||||
@@ -126,6 +134,7 @@ object DownloadController {
|
||||
body<EnqueueInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val inputs = json.decodeFromString<EnqueueInput>(ctx.body())
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -149,6 +158,7 @@ object DownloadController {
|
||||
body<EnqueueInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val input = json.decodeFromString<EnqueueInput>(ctx.body())
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -173,6 +183,7 @@ object DownloadController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, chapterIndex, mangaId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
DownloadManager.dequeue(chapterIndex, mangaId)
|
||||
|
||||
ctx.status(200)
|
||||
@@ -194,7 +205,8 @@ object DownloadController {
|
||||
description("Reorder chapter in download queue")
|
||||
}
|
||||
},
|
||||
behaviorOf = { _, chapterIndex, mangaId, to ->
|
||||
behaviorOf = { ctx, chapterIndex, mangaId, to ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
DownloadManager.reorder(chapterIndex, mangaId, to)
|
||||
},
|
||||
withResults = {
|
||||
|
||||
@@ -12,7 +12,10 @@ 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
|
||||
@@ -31,6 +34,7 @@ object ExtensionController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
ExtensionsList.getExtensionList()
|
||||
@@ -55,6 +59,7 @@ object ExtensionController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pkgName ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
Extension.installExtension(pkgName)
|
||||
@@ -84,6 +89,7 @@ object ExtensionController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val uploadedFile = ctx.uploadedFile("file")!!
|
||||
logger.debug { "Uploaded extension file name: " + uploadedFile.filename() }
|
||||
|
||||
@@ -116,6 +122,7 @@ object ExtensionController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pkgName ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
Extension.updateExtension(pkgName)
|
||||
@@ -143,6 +150,7 @@ object ExtensionController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pkgName ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Extension.uninstallExtension(pkgName)
|
||||
ctx.status(200)
|
||||
},
|
||||
@@ -165,6 +173,7 @@ object ExtensionController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, apkName ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Extension.getExtensionIcon(apkName) }
|
||||
.thenApply {
|
||||
|
||||
@@ -26,7 +26,10 @@ import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
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.formParam
|
||||
import suwayomi.tachidesk.server.util.handler
|
||||
import suwayomi.tachidesk.server.util.pathParam
|
||||
@@ -49,6 +52,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, onlineFetch ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
Manga.getManga(mangaId, onlineFetch)
|
||||
@@ -73,6 +77,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, onlineFetch ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
Manga.getMangaFull(mangaId, onlineFetch)
|
||||
@@ -96,6 +101,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Manga.getMangaThumbnail(mangaId) }
|
||||
.thenApply {
|
||||
@@ -123,6 +129,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Library.addMangaToLibrary(mangaId) }
|
||||
.thenApply { ctx.status(HttpStatus.OK) }
|
||||
@@ -145,6 +152,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Library.removeMangaFromLibrary(mangaId) }
|
||||
.thenApply { ctx.status(HttpStatus.OK) }
|
||||
@@ -167,6 +175,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(CategoryManga.getMangaCategories(mangaId))
|
||||
},
|
||||
withResults = {
|
||||
@@ -186,6 +195,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, categoryId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
CategoryManga.addMangaToCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
},
|
||||
@@ -206,6 +216,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, categoryId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
CategoryManga.removeMangaFromCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
},
|
||||
@@ -227,6 +238,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, key, value ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Manga.modifyMangaMeta(mangaId, key, value)
|
||||
ctx.status(200)
|
||||
},
|
||||
@@ -252,6 +264,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, onlineFetch ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Chapter.getChapterList(mangaId, onlineFetch) }
|
||||
.thenApply { ctx.json(it) }
|
||||
@@ -275,6 +288,7 @@ object MangaController {
|
||||
body<Chapter.MangaChapterBatchEditInput>()
|
||||
},
|
||||
behaviorOf = { ctx, mangaId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val input = json.decodeFromString<Chapter.MangaChapterBatchEditInput>(ctx.body())
|
||||
Chapter.modifyChapters(input, mangaId)
|
||||
},
|
||||
@@ -294,6 +308,7 @@ object MangaController {
|
||||
body<Chapter.ChapterBatchEditInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
|
||||
Chapter.modifyChapters(
|
||||
Chapter.MangaChapterBatchEditInput(
|
||||
@@ -320,6 +335,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, chapterIndex ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
var chapter = getChapterDownloadReadyByIndex(chapterIndex, mangaId)
|
||||
@@ -368,6 +384,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val chapterId = Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
|
||||
|
||||
// Sync with KoreaderSync when progress is updated
|
||||
@@ -394,6 +411,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, chapterIndex ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Chapter.deleteChapter(mangaId, chapterIndex)
|
||||
|
||||
ctx.status(200)
|
||||
@@ -418,6 +436,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, chapterIndex, key, value ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value)
|
||||
|
||||
ctx.status(200)
|
||||
@@ -445,6 +464,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, chapterIndex, index, updateProgress, format ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Page.getPageImage(mangaId, chapterIndex, index, format, null) }
|
||||
.thenApply {
|
||||
@@ -480,6 +500,7 @@ object MangaController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, chapterId, markAsRead ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
if (ctx.method() == HandlerType.HEAD) {
|
||||
ctx.future {
|
||||
future { ChapterDownloadHelper.getCbzMetadataForDownload(chapterId) }
|
||||
|
||||
@@ -17,7 +17,10 @@ import suwayomi.tachidesk.manga.impl.Source
|
||||
import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||
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.queryParam
|
||||
@@ -35,6 +38,7 @@ object SourceController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(Source.getSourceList())
|
||||
},
|
||||
withResults = {
|
||||
@@ -53,6 +57,7 @@ object SourceController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(Source.getSource(sourceId)!!)
|
||||
},
|
||||
withResults = {
|
||||
@@ -73,6 +78,7 @@ object SourceController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, pageNum ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
MangaList.getMangaList(sourceId, pageNum, popular = true)
|
||||
@@ -96,6 +102,7 @@ object SourceController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, pageNum ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
MangaList.getMangaList(sourceId, pageNum, popular = false)
|
||||
@@ -118,6 +125,7 @@ object SourceController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(Source.getSourcePreferences(sourceId))
|
||||
},
|
||||
withResults = {
|
||||
@@ -137,6 +145,7 @@ object SourceController {
|
||||
body<SourcePreferenceChange>()
|
||||
},
|
||||
behaviorOf = { ctx, sourceId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
|
||||
ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
|
||||
},
|
||||
@@ -157,6 +166,7 @@ object SourceController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, reset ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(Search.getFilterList(sourceId, reset))
|
||||
},
|
||||
withResults = {
|
||||
@@ -179,6 +189,7 @@ object SourceController {
|
||||
body<Array<FilterChange>>()
|
||||
},
|
||||
behaviorOf = { ctx, sourceId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val filterChange =
|
||||
try {
|
||||
json.decodeFromString<List<FilterChange>>(ctx.body())
|
||||
@@ -206,6 +217,7 @@ object SourceController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, searchTerm, pageNum ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Search.sourceSearch(sourceId, searchTerm, pageNum) }
|
||||
.thenApply { ctx.json(it) }
|
||||
@@ -229,6 +241,7 @@ object SourceController {
|
||||
body<FilterData>()
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, pageNum ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val filter = json.decodeFromString<FilterData>(ctx.body())
|
||||
ctx.future {
|
||||
future { Search.sourceFilter(sourceId, pageNum, filter) }
|
||||
@@ -251,6 +264,7 @@ object SourceController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, searchTerm ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
// TODO
|
||||
ctx.json(Search.sourceGlobalSearch(searchTerm))
|
||||
},
|
||||
|
||||
@@ -12,7 +12,10 @@ import io.javalin.http.HttpStatus
|
||||
import kotlinx.serialization.json.Json
|
||||
import suwayomi.tachidesk.manga.impl.track.Track
|
||||
import suwayomi.tachidesk.manga.model.dataclass.TrackerDataClass
|
||||
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.queryParam
|
||||
@@ -33,6 +36,7 @@ object TrackController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.json(Track.getTrackerList())
|
||||
},
|
||||
withResults = {
|
||||
@@ -50,6 +54,7 @@ object TrackController {
|
||||
body<Track.LoginInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val input = json.decodeFromString<Track.LoginInput>(ctx.body())
|
||||
logger.debug { "tracker login $input" }
|
||||
ctx.future {
|
||||
@@ -73,6 +78,7 @@ object TrackController {
|
||||
body<Track.LogoutInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val input = json.decodeFromString<Track.LogoutInput>(ctx.body())
|
||||
logger.debug { "tracker logout $input" }
|
||||
ctx.future {
|
||||
@@ -96,6 +102,7 @@ object TrackController {
|
||||
body<Track.SearchInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val input = json.decodeFromString<Track.SearchInput>(ctx.body())
|
||||
logger.debug { "tracker search $input" }
|
||||
ctx.future {
|
||||
@@ -122,6 +129,7 @@ object TrackController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, mangaId, trackerId, remoteId, private ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Track.bind(mangaId, trackerId, remoteId.toLong(), private) }
|
||||
.thenApply { ctx.status(HttpStatus.OK) }
|
||||
@@ -142,6 +150,7 @@ object TrackController {
|
||||
body<Track.UpdateInput>()
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val input = json.decodeFromString<Track.UpdateInput>(ctx.body())
|
||||
logger.debug { "tracker update $input" }
|
||||
ctx.future {
|
||||
@@ -164,6 +173,7 @@ object TrackController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, trackerId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Track.getTrackerThumbnail(trackerId) }
|
||||
.thenApply {
|
||||
|
||||
@@ -10,7 +10,10 @@ import suwayomi.tachidesk.manga.impl.update.UpdateStatus
|
||||
import suwayomi.tachidesk.manga.impl.update.UpdaterSocket
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
|
||||
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.formParam
|
||||
import suwayomi.tachidesk.server.util.handler
|
||||
import suwayomi.tachidesk.server.util.pathParam
|
||||
@@ -39,6 +42,7 @@ object UpdateController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pageNum ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future {
|
||||
Chapter.getRecentChapters(pageNum)
|
||||
@@ -66,6 +70,7 @@ object UpdateController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, categoryId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val updater = Injekt.get<IUpdater>()
|
||||
if (categoryId == null) {
|
||||
logger.info { "Adding Library to Update Queue" }
|
||||
@@ -96,6 +101,7 @@ object UpdateController {
|
||||
|
||||
fun categoryUpdateWS(ws: WsConfig) {
|
||||
ws.onConnect { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
UpdaterSocket.addClient(ctx)
|
||||
}
|
||||
ws.onMessage { ctx ->
|
||||
@@ -115,6 +121,7 @@ object UpdateController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val updater = Injekt.get<IUpdater>()
|
||||
ctx.json(updater.statusDeprecated.value)
|
||||
},
|
||||
@@ -132,6 +139,7 @@ object UpdateController {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val updater = Injekt.get<IUpdater>()
|
||||
logger.info { "Resetting Updater" }
|
||||
ctx.future {
|
||||
|
||||
@@ -9,7 +9,10 @@ import suwayomi.tachidesk.opds.dto.OpdsMangaFilter
|
||||
import suwayomi.tachidesk.opds.dto.OpdsSearchCriteria
|
||||
import suwayomi.tachidesk.opds.dto.PrimaryFilterType
|
||||
import suwayomi.tachidesk.opds.impl.OpdsFeedBuilder
|
||||
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.queryParam
|
||||
@@ -62,6 +65,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.contentType(OPDS_MIME).result(OpdsFeedBuilder.getRootFeed(BASE_URL, locale))
|
||||
},
|
||||
@@ -84,6 +88,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pageNumber, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -109,6 +114,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.contentType("application/opensearchdescription+xml").result(
|
||||
"""
|
||||
@@ -136,6 +142,7 @@ object OpdsV1Controller {
|
||||
handler(
|
||||
documentWith = { withOperation { summary("OPDS Series in Library Feed") } },
|
||||
behaviorOf = { ctx ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val pageNumber = ctx.queryParam("pageNumber")?.toIntOrNull()
|
||||
val query = ctx.queryParam("query")
|
||||
val author = ctx.queryParam("author")
|
||||
@@ -188,6 +195,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pageNumber, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -214,6 +222,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pageNumber, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -240,6 +249,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pageNumber, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -266,6 +276,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pageNumber, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -291,6 +302,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -316,6 +328,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -342,6 +355,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, pageNumber, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -370,6 +384,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, sourceId, pageNumber, sort, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -405,6 +420,7 @@ object OpdsV1Controller {
|
||||
pathParam<Long>("sourceId"),
|
||||
documentWith = { withOperation { summary("OPDS Library Source Specific Series Feed") } },
|
||||
behaviorOf = { ctx, sourceId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(sourceId = sourceId, primaryFilter = PrimaryFilterType.SOURCE))
|
||||
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
|
||||
},
|
||||
@@ -422,6 +438,7 @@ object OpdsV1Controller {
|
||||
pathParam<Int>("categoryId"),
|
||||
documentWith = { withOperation { summary("OPDS Category Specific Series Feed") } },
|
||||
behaviorOf = { ctx, categoryId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val criteria =
|
||||
buildCriteriaFromContext(ctx, OpdsMangaFilter(categoryId = categoryId, primaryFilter = PrimaryFilterType.CATEGORY))
|
||||
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
|
||||
@@ -440,6 +457,7 @@ object OpdsV1Controller {
|
||||
pathParam<String>("genre"),
|
||||
documentWith = { withOperation { summary("OPDS Genre Specific Series Feed") } },
|
||||
behaviorOf = { ctx, genre ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(genre = genre, primaryFilter = PrimaryFilterType.GENRE))
|
||||
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
|
||||
},
|
||||
@@ -457,6 +475,7 @@ object OpdsV1Controller {
|
||||
pathParam<Int>("statusId"),
|
||||
documentWith = { withOperation { summary("OPDS Status Specific Series Feed") } },
|
||||
behaviorOf = { ctx, statusId ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(statusId = statusId, primaryFilter = PrimaryFilterType.STATUS))
|
||||
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
|
||||
},
|
||||
@@ -479,6 +498,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, langCode ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val criteria =
|
||||
buildCriteriaFromContext(ctx, OpdsMangaFilter(langCode = langCode, primaryFilter = PrimaryFilterType.LANGUAGE))
|
||||
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
|
||||
@@ -506,6 +526,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, seriesId, pageNumber, sort, filter, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
@@ -536,6 +557,7 @@ object OpdsV1Controller {
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, seriesId, chapterIndex, lang ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
|
||||
ctx.future {
|
||||
future {
|
||||
|
||||
@@ -12,12 +12,14 @@ import gg.jte.TemplateEngine
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.Javalin
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
import io.javalin.http.Context
|
||||
import io.javalin.http.HandlerType
|
||||
import io.javalin.http.HttpStatus
|
||||
import io.javalin.http.RedirectResponse
|
||||
import io.javalin.http.UnauthorizedResponse
|
||||
import io.javalin.http.staticfiles.Location
|
||||
import io.javalin.rendering.template.JavalinJte
|
||||
import io.javalin.websocket.WsContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -31,6 +33,11 @@ import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.i18n.LocalizationHelper
|
||||
import suwayomi.tachidesk.manga.MangaAPI
|
||||
import suwayomi.tachidesk.opds.OpdsAPI
|
||||
import suwayomi.tachidesk.server.user.ForbiddenException
|
||||
import suwayomi.tachidesk.server.user.UnauthorizedException
|
||||
import suwayomi.tachidesk.server.user.UserType
|
||||
import suwayomi.tachidesk.server.user.getUserFromContext
|
||||
import suwayomi.tachidesk.server.user.getUserFromWsContext
|
||||
import suwayomi.tachidesk.server.util.Browser
|
||||
import suwayomi.tachidesk.server.util.WebInterfaceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -207,6 +214,8 @@ object JavalinSetup {
|
||||
ctx.header("WWW-Authenticate", "Basic")
|
||||
throw UnauthorizedResponse()
|
||||
}
|
||||
|
||||
ctx.setAttribute(Attribute.TachideskUser, getUserFromContext(ctx))
|
||||
}
|
||||
|
||||
app.events { event ->
|
||||
@@ -217,6 +226,12 @@ object JavalinSetup {
|
||||
}
|
||||
}
|
||||
|
||||
app.wsBefore {
|
||||
it.onConnect { ctx ->
|
||||
ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
thread(start = false) {
|
||||
@@ -244,6 +259,18 @@ object JavalinSetup {
|
||||
ctx.result(e.message ?: "Bad Request")
|
||||
}
|
||||
|
||||
app.exception(UnauthorizedException::class.java) { e, ctx ->
|
||||
logger.error(e) { "UnauthorizedException while handling the request" }
|
||||
ctx.status(HttpStatus.UNAUTHORIZED)
|
||||
ctx.result(e.message ?: "Unauthorized")
|
||||
}
|
||||
|
||||
app.exception(ForbiddenException::class.java) { e, ctx ->
|
||||
logger.error(e) { "ForbiddenException while handling the request" }
|
||||
ctx.status(HttpStatus.FORBIDDEN)
|
||||
ctx.result(e.message ?: "Forbidden")
|
||||
}
|
||||
|
||||
app.start()
|
||||
}
|
||||
|
||||
@@ -262,4 +289,28 @@ object JavalinSetup {
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
sealed class Attribute<T : Any>(
|
||||
val name: String,
|
||||
) {
|
||||
data object TachideskUser : Attribute<UserType>("user")
|
||||
}
|
||||
|
||||
private fun <T : Any> Context.setAttribute(
|
||||
attribute: Attribute<T>,
|
||||
value: T,
|
||||
) {
|
||||
attribute(attribute.name, value)
|
||||
}
|
||||
|
||||
private fun <T : Any> WsContext.setAttribute(
|
||||
attribute: Attribute<T>,
|
||||
value: T,
|
||||
) {
|
||||
attribute(attribute.name, value)
|
||||
}
|
||||
|
||||
fun <T : Any> Context.getAttribute(attribute: Attribute<T>): T = attribute(attribute.name)!!
|
||||
|
||||
fun <T : Any> WsContext.getAttribute(attribute: Attribute<T>): T = attribute(attribute.name)!!
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import suwayomi.tachidesk.graphql.types.WebUIInterface
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.time.Duration
|
||||
|
||||
val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
@@ -152,6 +153,9 @@ class ServerConfig(
|
||||
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue()
|
||||
val authUsername: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val authPassword: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val jwtAudience: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val jwtTokenExpiry: MutableStateFlow<Duration> by OverrideConfigValue()
|
||||
val jwtRefreshExpiry: MutableStateFlow<Duration> by OverrideConfigValue()
|
||||
val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue({
|
||||
authMode.value == AuthMode.BASIC_AUTH
|
||||
}) {
|
||||
|
||||
@@ -48,6 +48,7 @@ import suwayomi.tachidesk.manga.impl.util.lang.renameTo
|
||||
import suwayomi.tachidesk.server.database.databaseUp
|
||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
||||
import suwayomi.tachidesk.server.util.DurationType
|
||||
import suwayomi.tachidesk.server.util.MutableStateFlowType
|
||||
import suwayomi.tachidesk.server.util.SystemTray
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -176,6 +177,7 @@ fun applicationSetup() {
|
||||
|
||||
// register Tachidesk's config which is dubbed "ServerConfig"
|
||||
registerCustomType(MutableStateFlowType())
|
||||
registerCustomType(DurationType())
|
||||
GlobalConfigManager.registerModule(
|
||||
ServerConfig.register { GlobalConfigManager.config },
|
||||
)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package suwayomi.tachidesk.server.user
|
||||
|
||||
import io.javalin.http.Context
|
||||
import io.javalin.http.Header
|
||||
import io.javalin.websocket.WsConnectContext
|
||||
import suwayomi.tachidesk.global.impl.util.Jwt
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
|
||||
sealed class UserType {
|
||||
class Admin(
|
||||
val id: Int,
|
||||
) : UserType()
|
||||
|
||||
data object Visitor : UserType()
|
||||
}
|
||||
|
||||
fun UserType.requireUser(): Int =
|
||||
when (this) {
|
||||
is UserType.Admin -> id
|
||||
UserType.Visitor -> throw UnauthorizedException()
|
||||
}
|
||||
|
||||
fun getUserFromToken(token: String?): UserType {
|
||||
if (serverConfig.authMode.value != AuthMode.UI_LOGIN) {
|
||||
return UserType.Admin(1)
|
||||
}
|
||||
|
||||
if (token.isNullOrBlank()) {
|
||||
return UserType.Visitor
|
||||
}
|
||||
|
||||
return Jwt.verifyJwt(token)
|
||||
}
|
||||
|
||||
fun getUserFromContext(ctx: Context): UserType {
|
||||
val authentication = ctx.header(Header.AUTHORIZATION) ?: ctx.cookie("suwayomi-server-token")
|
||||
val token = authentication?.substringAfter("Bearer ") ?: ctx.queryParam("token")
|
||||
|
||||
return getUserFromToken(token)
|
||||
}
|
||||
|
||||
fun getUserFromWsContext(ctx: WsConnectContext): UserType {
|
||||
val authentication =
|
||||
ctx.header(Header.AUTHORIZATION) ?: ctx.header("Sec-WebSocket-Protocol") ?: ctx.cookie("suwayomi-server-token")
|
||||
val token = authentication?.substringAfter("Bearer ") ?: ctx.queryParam("token")
|
||||
|
||||
return getUserFromToken(token)
|
||||
}
|
||||
|
||||
class UnauthorizedException : IllegalStateException("Unauthorized")
|
||||
|
||||
class ForbiddenException : IllegalStateException("Forbidden")
|
||||
@@ -0,0 +1,31 @@
|
||||
package suwayomi.tachidesk.server.util
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import io.github.config4k.ClassContainer
|
||||
import io.github.config4k.CustomType
|
||||
import io.github.config4k.readers.SelectReader
|
||||
import io.github.config4k.toConfig
|
||||
import kotlin.time.Duration
|
||||
|
||||
class DurationType : CustomType {
|
||||
override fun parse(
|
||||
clazz: ClassContainer,
|
||||
config: Config,
|
||||
name: String,
|
||||
): Any? {
|
||||
val clazz = ClassContainer(String::class)
|
||||
val reader = SelectReader.getReader(clazz)
|
||||
val path = name
|
||||
val result = reader(config, path) as String
|
||||
return Duration.parse(result)
|
||||
}
|
||||
|
||||
override fun testParse(clazz: ClassContainer): Boolean = clazz.mapperClass.qualifiedName == "kotlin.time.Duration"
|
||||
|
||||
override fun testToConfig(obj: Any): Boolean = obj as? Duration != null
|
||||
|
||||
override fun toConfig(
|
||||
obj: Any,
|
||||
name: String,
|
||||
): Config = (obj as Duration).toString().toConfig(name)
|
||||
}
|
||||
@@ -49,9 +49,12 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha
|
||||
server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update
|
||||
|
||||
# Authentication
|
||||
server.authMode = "none" # none, basic_auth or simple_login
|
||||
server.authMode = "none" # none, basic_auth, simple_login or ui_login
|
||||
server.authUsername = ""
|
||||
server.authPassword = ""
|
||||
server.jwtAudience = "suwayomi-server-api"
|
||||
server.jwtTokenExpiry = "5m"
|
||||
server.jwtRefreshExpiry = "60d"
|
||||
|
||||
# misc
|
||||
server.debugLogsEnabled = false
|
||||
|
||||
@@ -49,9 +49,12 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha
|
||||
server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update
|
||||
|
||||
# Authentication
|
||||
server.authMode = "none" # none, basic_auth or simple_login
|
||||
server.authMode = "none" # none, basic_auth, simple_login or ui_login
|
||||
server.authUsername = ""
|
||||
server.authPassword = ""
|
||||
server.jwtAudience = "suwayomi-server-api"
|
||||
server.jwtTokenExpiry = "5m"
|
||||
server.jwtRefreshExpiry = "60d"
|
||||
|
||||
# misc
|
||||
server.debugLogsEnabled = false
|
||||
|
||||
Reference in New Issue
Block a user