Add support to login at Tsuki. (#7715)

This commit is contained in:
Alessandro Jean 2021-06-18 06:52:23 -03:00 committed by GitHub
parent 7916260467
commit 9ea926b8a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 145 additions and 9 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Tsuki Mangás' extName = 'Tsuki Mangás'
pkgNameSuffix = 'pt.tsukimangas' pkgNameSuffix = 'pt.tsukimangas'
extClass = '.TsukiMangas' extClass = '.TsukiMangas'
extVersionCode = 19 extVersionCode = 20
libVersion = '1.2' libVersion = '1.2'
containsNsfw = true containsNsfw = true
} }

View File

@ -1,9 +1,16 @@
package eu.kanade.tachiyomi.extension.pt.tsukimangas package eu.kanade.tachiyomi.extension.pt.tsukimangas
import android.app.Application
import android.content.SharedPreferences
import android.text.InputType
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -11,16 +18,22 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.text.ParseException import java.text.ParseException
@ -28,7 +41,7 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@Nsfw @Nsfw
class TsukiMangas : HttpSource() { class TsukiMangas : HttpSource(), ConfigurableSource {
override val name = "Tsuki Mangás" override val name = "Tsuki Mangás"
@ -42,17 +55,29 @@ class TsukiMangas : HttpSource() {
.addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 1)) .addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 1))
.addInterceptor(SpecificHostRateLimitInterceptor(CDN_1_URL, 1, period = 2)) .addInterceptor(SpecificHostRateLimitInterceptor(CDN_1_URL, 1, period = 2))
.addInterceptor(SpecificHostRateLimitInterceptor(CDN_2_URL, 1, period = 2)) .addInterceptor(SpecificHostRateLimitInterceptor(CDN_2_URL, 1, period = 2))
.addInterceptor(::tsukiPermissionIntercept) .addInterceptor(::tsukiAuthIntercept)
.build() .build()
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val usernameOrEmail: String
get() = preferences.getString(USERNAME_OR_EMAIL_PREF_KEY, "")!!
private val password: String
get() = preferences.getString(PASSWORD_PREF_KEY, "")!!
private var apiToken: String? = null
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Accept", ACCEPT) .add("Accept", ACCEPT)
.add("Accept-Language", ACCEPT_LANGUAGE) .add("Accept-Language", ACCEPT_LANGUAGE)
.add("User-Agent", USER_AGENT) .add("User-Agent", USER_AGENT)
.add("Referer", baseUrl) .add("Referer", baseUrl)
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/api/v2/mangas?page=$page&title=&adult_content=false&filter=0", headers) return GET("$baseUrl/api/v2/mangas?page=$page&title=&adult_content=false&filter=0", headers)
} }
@ -295,17 +320,98 @@ class TsukiMangas : HttpSource() {
return GET(page.imageUrl!!, newHeaders) return GET(page.imageUrl!!, newHeaders)
} }
private fun tsukiPermissionIntercept(chain: Interceptor.Chain): Response { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val response = chain.proceed(chain.request()) val usernameOrEmailPref = EditTextPreference(screen.context).apply {
key = USERNAME_OR_EMAIL_PREF_KEY
title = USERNAME_OR_EMAIL_PREF_TITLE
setDefaultValue("")
summary = USERNAME_OR_EMAIL_PREF_SUMMARY
dialogTitle = USERNAME_OR_EMAIL_PREF_TITLE
if (response.code == 403) { setOnPreferenceChangeListener { _, newValue ->
apiToken = null
preferences.edit()
.putString(USERNAME_OR_EMAIL_PREF_KEY, newValue as String)
.commit()
}
}
val passwordPref = EditTextPreference(screen.context).apply {
key = PASSWORD_PREF_KEY
title = PASSWORD_PREF_TITLE
setDefaultValue("")
summary = PASSWORD_PREF_SUMMARY
dialogTitle = PASSWORD_PREF_TITLE
setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
setOnPreferenceChangeListener { _, newValue ->
apiToken = null
preferences.edit()
.putString(PASSWORD_PREF_KEY, newValue as String)
.commit()
}
}
screen.addPreference(usernameOrEmailPref)
screen.addPreference(passwordPref)
}
private fun tsukiAuthIntercept(chain: Interceptor.Chain): Response {
if (apiToken.isNullOrEmpty()) {
if (usernameOrEmail.isEmpty() || password.isEmpty()) {
throw IOException(ERROR_CREDENTIALS_MISSING)
}
val loginRequest = loginRequest(usernameOrEmail, password)
val loginResponse = chain.proceed(loginRequest)
// API returns 422 when the credentials are invalid.
if (loginResponse.code == 422) {
loginResponse.close()
throw IOException(ERROR_CANNOT_LOGIN)
}
try {
val loginResponseBody = loginResponse.body?.string().orEmpty()
val authResult = json.decodeFromString<TsukiAuthResultDto>(loginResponseBody)
apiToken = authResult.token
loginResponse.close()
} catch (e: SerializationException) {
loginResponse.close()
throw IOException(ERROR_LOGIN_FAILED)
}
}
val authorizedRequest = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $apiToken")
.build()
val response = chain.proceed(authorizedRequest)
// API returns 403 when User-Agent permission is disabled
// and returns 401 when the token is invalid.
if (response.code == 403 || response.code == 401) {
response.close() response.close()
throw IOException(UA_DISABLED_MESSAGE) throw IOException(if (response.code == 403) UA_DISABLED_MESSAGE else ERROR_INVALID_TOKEN)
} }
return response return response
} }
private fun loginRequest(usernameOrEmail: String, password: String): Request {
val authInfo = TsukiAuthRequestDto(usernameOrEmail, password)
val payload = json.encodeToString(authInfo).toRequestBody(JSON_MEDIA_TYPE)
return POST("$baseUrl/api/v2/login", headers, payload)
}
private class Genre(name: String) : Filter.CheckBox(name) private class Genre(name: String) : Filter.CheckBox(name)
private class DemographyFilter(demographies: List<String>) : Filter.Select<String>("Demografia", demographies.toTypedArray()) private class DemographyFilter(demographies: List<String>) : Filter.Select<String>("Demografia", demographies.toTypedArray())
@ -470,5 +576,24 @@ class TsukiMangas : HttpSource() {
const val PREFIX_ID_SEARCH = "id:" const val PREFIX_ID_SEARCH = "id:"
private val ID_SEARCH_PATTERN = "^id:(\\d+)$".toRegex() private val ID_SEARCH_PATTERN = "^id:(\\d+)$".toRegex()
private val JSON_MEDIA_TYPE = "application/json;charset=UTF-8".toMediaType()
private const val USERNAME_OR_EMAIL_PREF_KEY = "username_or_email"
private const val USERNAME_OR_EMAIL_PREF_TITLE = "Usuário ou E-mail"
private const val USERNAME_OR_EMAIL_PREF_SUMMARY = "Defina aqui o seu usuário ou e-mail para o login."
private const val PASSWORD_PREF_KEY = "password"
private const val PASSWORD_PREF_TITLE = "Senha"
private const val PASSWORD_PREF_SUMMARY = "Defina aqui a sua senha para o login."
private const val ERROR_CANNOT_LOGIN = "Não foi possível realizar o login. " +
"Revise suas informações nas configurações."
private const val ERROR_CREDENTIALS_MISSING = "Login necessário. " +
"Defina suas credenciais de acesso nas configurações."
private const val ERROR_LOGIN_FAILED = "Não foi possível realizar o login devido " +
"a um erro inesperado. Tente novamente mais tarde."
private const val ERROR_INVALID_TOKEN = "Token inválido. " +
"Revise suas informações nas configurações."
} }
} }

View File

@ -3,6 +3,17 @@ package eu.kanade.tachiyomi.extension.pt.tsukimangas
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
data class TsukiAuthRequestDto(
val username: String,
val password: String
)
@Serializable
data class TsukiAuthResultDto(
val token: String? = null
)
@Serializable @Serializable
data class TsukiPaginatedDto( data class TsukiPaginatedDto(
val data: List<TsukiMangaDto> = emptyList(), val data: List<TsukiMangaDto> = emptyList(),