Kitsu auth implementation

This commit is contained in:
Koitharu
2024-01-30 15:41:03 +02:00
parent d0ee185d2e
commit 5687ca6e96
11 changed files with 107 additions and 17 deletions

View File

@@ -8,7 +8,10 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.network.CurlLoggingInterceptor
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListAuthenticator
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListInterceptor
import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler

View File

@@ -249,7 +249,7 @@ class AniListRepository @Inject constructor(
private fun AniListUser(json: JSONObject) = ScrobblerUser(
id = json.getLong("id"),
nickname = json.getString("name"),
avatar = json.getJSONObject("avatar").getString("medium"),
avatar = json.getJSONObject("avatar").getStringOrNull("medium"),
service = ScrobblerService.ANILIST,
)

View File

@@ -3,6 +3,6 @@ package org.koitharu.kotatsu.scrobbling.common.domain.model
data class ScrobblerUser(
val id: Long,
val nickname: String,
val avatar: String,
val avatar: String?,
val service: ScrobblerService,
)

View File

@@ -111,7 +111,7 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
return
}
viewBinding.imageViewAvatar.newImageRequest(this, user.avatar)
?.placeholder(R.drawable.bg_badge_empty)
?.placeholder(R.drawable.ic_shortcut_default)
?.enqueueWith(coil)
}
@@ -136,6 +136,7 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
const val HOST_ANILIST_AUTH = "anilist-auth"
const val HOST_MAL_AUTH = "mal-auth"
const val HOST_KITSU_AUTH = "kitsu-auth"
fun newIntent(context: Context, service: ScrobblerService) =
Intent(context, ScrobblerConfigActivity::class.java)

View File

@@ -109,6 +109,7 @@ class ScrobblerConfigViewModel @Inject constructor(
ScrobblerConfigActivity.HOST_SHIKIMORI_AUTH -> ScrobblerService.SHIKIMORI
ScrobblerConfigActivity.HOST_ANILIST_AUTH -> ScrobblerService.ANILIST
ScrobblerConfigActivity.HOST_MAL_AUTH -> ScrobblerService.MAL
ScrobblerConfigActivity.HOST_KITSU_AUTH -> ScrobblerService.KITSU
else -> error("Wrong scrobbler uri: $uri")
}
}

View File

@@ -1,9 +1,12 @@
package org.koitharu.kotatsu.scrobbling.kitsu.data
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
@@ -16,7 +19,37 @@ class KitsuAuthenticator @Inject constructor(
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
TODO("Not yet implemented")
val accessToken = storage.accessToken ?: return null
if (!isRequestWithAccessToken(response)) {
return null
}
synchronized(this) {
val newAccessToken = storage.accessToken ?: return null
if (accessToken != newAccessToken) {
return newRequestWithAccessToken(response.request, newAccessToken)
}
val updatedAccessToken = refreshAccessToken() ?: return null
return newRequestWithAccessToken(response.request, updatedAccessToken)
}
}
private fun isRequestWithAccessToken(response: Response): Boolean {
val header = response.request.header(CommonHeaders.AUTHORIZATION)
return header?.startsWith("Bearer") == true
}
private fun newRequestWithAccessToken(request: Request, accessToken: String): Request {
return request.newBuilder()
.header(CommonHeaders.AUTHORIZATION, "Bearer $accessToken")
.build()
}
private fun refreshAccessToken(): String? = runCatching {
val repository = repositoryProvider.get()
runBlocking { repository.authorize(null) }
return storage.accessToken
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
}

View File

@@ -5,7 +5,7 @@ import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
private const val JSON = "application/json"
private const val JSON = "application/vnd.api+json"
class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor {

View File

@@ -9,7 +9,10 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.urlEncoded
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
@@ -43,8 +46,8 @@ class KitsuRepository(
val body = FormBody.Builder()
if (code != null) {
body.add("grant_type", "password")
body.add("username", "test@test")
body.add("password", "test")
body.add("username", code.substringBefore(';'))
body.add("password", code.substringAfter(';'))
} else {
body.add("grant_type", "refresh_token")
body.add("refresh_token", checkNotNull(storage.refreshToken))
@@ -58,11 +61,22 @@ class KitsuRepository(
}
override suspend fun loadUser(): ScrobblerUser {
TODO("Not yet implemented")
val request = Request.Builder()
.get()
.url("${BASE_WEB_URL}/api/edge/users?filter[self]=true")
val response = okHttp.newCall(request.build()).await().parseJson()
.getJSONArray("data")
.getJSONObject(0)
return ScrobblerUser(
id = response.getLongOrDefault("id", 0L),
nickname = response.getJSONObject("attributes").getString("name"),
avatar = response.getJSONObject("attributes").optJSONObject("avatar")?.getStringOrNull("small"),
service = ScrobblerService.KITSU,
)
}
override fun logout() {
TODO("Not yet implemented")
storage.clear()
}
override suspend fun unregister(mangaId: Long) {
@@ -70,7 +84,11 @@ class KitsuRepository(
}
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
TODO("Not yet implemented")
val request = Request.Builder()
.get()
.url("${BASE_WEB_URL}/api/edge/manga?page[limit]=20&page[offset]=$offset&filter[text]=${query.urlEncoded()}")
val response = okHttp.newCall(request.build()).await().parseJson()
return emptyList()
}
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {

View File

@@ -1,18 +1,28 @@
package org.koitharu.kotatsu.scrobbling.kitsu.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityKitsuAuthBinding
import org.koitharu.kotatsu.parsers.util.urlEncoded
class KitsuAuthActivity : BaseActivity<ActivityKitsuAuthBinding>() {
class KitsuAuthActivity : BaseActivity<ActivityKitsuAuthBinding>(), View.OnClickListener, TextWatcher {
private val regexEmail = Regex("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", RegexOption.IGNORE_CASE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityKitsuAuthBinding.inflate(layoutInflater))
viewBinding.buttonCancel.setOnClickListener(this)
viewBinding.buttonDone.setOnClickListener(this)
viewBinding.editEmail.addTextChangedListener(this)
viewBinding.editPassword.addTextChangedListener(this)
}
override fun onWindowInsetsChanged(insets: Insets) {
@@ -25,8 +35,32 @@ class KitsuAuthActivity : BaseActivity<ActivityKitsuAuthBinding>() {
)
}
companion object {
fun newIntent(context: Context) = Intent(context, KitsuAuthActivity::class.java)
override fun onClick(v: View) {
when (v.id) {
R.id.button_cancel -> finish()
R.id.button_done -> continueAuth()
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
val email = viewBinding.editEmail.text?.toString()?.trim()
val password = viewBinding.editPassword.text?.toString()?.trim()
viewBinding.buttonDone.isEnabled = !email.isNullOrEmpty()
&& !password.isNullOrEmpty()
&& regexEmail.matches(email)
&& password.length >= 3
}
private fun continueAuth() {
val email = viewBinding.editEmail.text?.toString()?.trim().orEmpty()
val password = viewBinding.editPassword.text?.toString()?.trim().orEmpty()
val url = "kotatsu://kitsu-auth?code=" + "$email;$password".urlEncoded()
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
finishAfterTransition()
}
}

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
@@ -29,7 +30,6 @@ import javax.inject.Singleton
private const val REDIRECT_URI = "kotatsu://mal-auth"
private const val BASE_WEB_URL = "https://myanimelist.net"
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
private const val AVATAR_STUB = "https://cdn.myanimelist.net/images/questionmark_50.gif"
@Singleton
class MALRepository @Inject constructor(
@@ -209,7 +209,7 @@ class MALRepository @Inject constructor(
private fun MALUser(json: JSONObject) = ScrobblerUser(
id = json.getLong("id"),
nickname = json.getString("name"),
avatar = json.getString("picture") ?: AVATAR_STUB,
avatar = json.getStringOrNull("picture"),
service = ScrobblerService.MAL,
)

View File

@@ -216,7 +216,7 @@ class ShikimoriRepository @Inject constructor(
private fun ShikimoriUser(json: JSONObject) = ScrobblerUser(
id = json.getLong("id"),
nickname = json.getString("nickname"),
avatar = json.getString("avatar"),
avatar = json.getStringOrNull("avatar"),
service = ScrobblerService.SHIKIMORI,
)
}