Kitsu auth implementation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user