diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt index f9f3b1187..e2703984d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt index bd4852bf5..42552bef4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt @@ -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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerUser.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerUser.kt index d79d0a3c7..9570734c9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerUser.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerUser.kt @@ -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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt index 52d42edf0..cf49379f4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt @@ -111,7 +111,7 @@ class ScrobblerConfigActivity : BaseActivity(), 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(), 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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt index c18aaca7a..1bf51ec23 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt @@ -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") } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt index 106651267..0d12cefe6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuAuthenticator.kt @@ -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() + } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt index 13b0069e3..1c4300d2b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuInterceptor.kt @@ -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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt index 1bac6bd17..e633f40cd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/data/KitsuRepository.kt @@ -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 { - 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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt index b01f14adb..a63a733c9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/kitsu/ui/KitsuAuthActivity.kt @@ -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() { +class KitsuAuthActivity : BaseActivity(), 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() { ) } - 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() + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt index d2887c30c..d3c028ee5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt @@ -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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt index 6bc2eb657..266932fdf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt @@ -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, ) }