Kitsu auth implementation

master
Koitharu 2 years ago
parent d0ee185d2e
commit 5687ca6e96
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -8,7 +8,10 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet import dagger.multibindings.ElementsIntoSet
import okhttp3.OkHttpClient 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.BaseHttpClient
import org.koitharu.kotatsu.core.network.CurlLoggingInterceptor
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListAuthenticator import org.koitharu.kotatsu.scrobbling.anilist.data.AniListAuthenticator
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListInterceptor import org.koitharu.kotatsu.scrobbling.anilist.data.AniListInterceptor
import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler

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

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

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

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

@ -1,9 +1,12 @@
package org.koitharu.kotatsu.scrobbling.kitsu.data package org.koitharu.kotatsu.scrobbling.kitsu.data
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator import okhttp3.Authenticator
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.Route 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.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
@ -16,7 +19,37 @@ class KitsuAuthenticator @Inject constructor(
) : Authenticator { ) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? { 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()
} }

@ -5,7 +5,7 @@ import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage 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 { class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor {

@ -9,7 +9,10 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.await 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.parseJson
import org.koitharu.kotatsu.parsers.util.urlEncoded
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
@ -43,8 +46,8 @@ class KitsuRepository(
val body = FormBody.Builder() val body = FormBody.Builder()
if (code != null) { if (code != null) {
body.add("grant_type", "password") body.add("grant_type", "password")
body.add("username", "test@test") body.add("username", code.substringBefore(';'))
body.add("password", "test") body.add("password", code.substringAfter(';'))
} else { } else {
body.add("grant_type", "refresh_token") body.add("grant_type", "refresh_token")
body.add("refresh_token", checkNotNull(storage.refreshToken)) body.add("refresh_token", checkNotNull(storage.refreshToken))
@ -58,11 +61,22 @@ class KitsuRepository(
} }
override suspend fun loadUser(): ScrobblerUser { 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() { override fun logout() {
TODO("Not yet implemented") storage.clear()
} }
override suspend fun unregister(mangaId: Long) { override suspend fun unregister(mangaId: Long) {
@ -70,7 +84,11 @@ class KitsuRepository(
} }
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> { 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 { override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {

@ -1,18 +1,28 @@
package org.koitharu.kotatsu.scrobbling.kitsu.ui package org.koitharu.kotatsu.scrobbling.kitsu.ui
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.net.toUri
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityKitsuAuthBinding 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityKitsuAuthBinding.inflate(layoutInflater)) 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) { override fun onWindowInsetsChanged(insets: Insets) {
@ -25,8 +35,32 @@ class KitsuAuthActivity : BaseActivity<ActivityKitsuAuthBinding>() {
) )
} }
companion object { override fun onClick(v: View) {
fun newIntent(context: Context) = Intent(context, KitsuAuthActivity::class.java) 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()
}
} }

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

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

Loading…
Cancel
Save