Remove auto-solve captcha
This commit is contained in:
parent
726626f2c5
commit
0784629cbb
@ -381,10 +381,6 @@
|
|||||||
<data android:pathPattern="/chapter/..*" />
|
<data android:pathPattern="/chapter/..*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name="exh.ui.captcha.BrowserActionActivity"
|
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
|
||||||
android:exported="false"/>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -16,7 +16,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import eu.kanade.domain.UnsortedPreferences
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.library.service.LibraryPreferences
|
import eu.kanade.domain.library.service.LibraryPreferences
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
@ -40,7 +39,6 @@ object SettingsGeneralScreen : SearchableSettings {
|
|||||||
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
|
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
|
||||||
// SY -->
|
// SY -->
|
||||||
val uiPrefs = remember { Injekt.get<UiPreferences>() }
|
val uiPrefs = remember { Injekt.get<UiPreferences>() }
|
||||||
val unsortedPrefs = remember { Injekt.get<UnsortedPreferences>() }
|
|
||||||
// SY <--
|
// SY <--
|
||||||
return mutableListOf<Preference>().apply {
|
return mutableListOf<Preference>().apply {
|
||||||
add(
|
add(
|
||||||
@ -104,11 +102,6 @@ object SettingsGeneralScreen : SearchableSettings {
|
|||||||
pref = uiPrefs.expandFilters(),
|
pref = uiPrefs.expandFilters(),
|
||||||
title = stringResource(R.string.toggle_expand_search_filters),
|
title = stringResource(R.string.toggle_expand_search_filters),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
pref = unsortedPrefs.autoSolveCaptcha(),
|
|
||||||
title = stringResource(R.string.auto_solve_captchas),
|
|
||||||
subtitle = stringResource(R.string.auto_solve_captchas_summary),
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = uiPrefs.recommendsInOverflow(),
|
pref = uiPrefs.recommendsInOverflow(),
|
||||||
title = stringResource(R.string.put_recommends_in_overflow),
|
title = stringResource(R.string.put_recommends_in_overflow),
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
package exh.patch
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import exh.ui.captcha.BrowserActionActivity
|
|
||||||
import exh.util.interceptAsHtml
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
private val HIDE_SCRIPT =
|
|
||||||
"""
|
|
||||||
document.querySelector("#forgot_button").style.visibility = "hidden";
|
|
||||||
document.querySelector("#signup_button").style.visibility = "hidden";
|
|
||||||
document.querySelector("#announcement").style.visibility = "hidden";
|
|
||||||
document.querySelector("nav").style.visibility = "hidden";
|
|
||||||
document.querySelector("footer").style.visibility = "hidden";
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
private fun verifyComplete(url: String): Boolean {
|
|
||||||
return url.toHttpUrlOrNull()?.let { parsed ->
|
|
||||||
parsed.host == "mangadex.org" && parsed.pathSegments.none { it.isNotBlank() }
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
val MANGADEX_LOGIN_PATCH: EHInterceptor = { request, response, sourceId ->
|
|
||||||
if (request.url.host == MANGADEX_DOMAIN) {
|
|
||||||
response.interceptAsHtml { doc ->
|
|
||||||
if (doc.title().trim().equals("Login - MangaDex", true)) {
|
|
||||||
BrowserActionActivity.launchAction(
|
|
||||||
Injekt.get<Application>(),
|
|
||||||
::verifyComplete,
|
|
||||||
HIDE_SCRIPT,
|
|
||||||
"https://mangadex.org/login",
|
|
||||||
"Login",
|
|
||||||
(Injekt.get<SourceManager>().get(sourceId) as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
|
||||||
it.value.joinToString(",")
|
|
||||||
} ?: emptyMap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val MANGADEX_SOURCE_IDS = listOf(
|
|
||||||
2499283573021220255,
|
|
||||||
8033579885162383068,
|
|
||||||
1952071260038453057,
|
|
||||||
2098905203823335614,
|
|
||||||
5098537545549490547,
|
|
||||||
4505830566611664829,
|
|
||||||
9194073792736219759,
|
|
||||||
6400665728063187402,
|
|
||||||
4938773340256184018,
|
|
||||||
5860541308324630662,
|
|
||||||
5189216366882819742,
|
|
||||||
2655149515337070132,
|
|
||||||
1145824452519314725,
|
|
||||||
3846770256925560569,
|
|
||||||
3807502156582598786,
|
|
||||||
4284949320785450865,
|
|
||||||
5463447640980279236,
|
|
||||||
8578871918181236609,
|
|
||||||
6750440049024086587,
|
|
||||||
3339599426223341161,
|
|
||||||
5148895169070562838,
|
|
||||||
1493666528525752601,
|
|
||||||
1713554459881080228,
|
|
||||||
4150470519566206911,
|
|
||||||
1347402746269051958,
|
|
||||||
3578612018159256808,
|
|
||||||
425785191804166217,
|
|
||||||
8254121249433835847,
|
|
||||||
3260701926561129943,
|
|
||||||
1411768577036936240,
|
|
||||||
3285208643537017688,
|
|
||||||
737986167355114438,
|
|
||||||
1471784905273036181,
|
|
||||||
5967745367608513818,
|
|
||||||
3781216447842245147,
|
|
||||||
4774459486579224459,
|
|
||||||
4710920497926776490,
|
|
||||||
5779037855201976894,
|
|
||||||
)
|
|
||||||
const val MANGADEX_DOMAIN = "mangadex.org"
|
|
@ -1,43 +0,0 @@
|
|||||||
package exh.patch
|
|
||||||
|
|
||||||
import eu.kanade.domain.UnsortedPreferences
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
typealias EHInterceptor = (request: Request, response: Response, sourceId: Long) -> Response
|
|
||||||
|
|
||||||
fun OkHttpClient.Builder.injectPatches(sourceIdProducer: () -> Long): OkHttpClient.Builder {
|
|
||||||
return addInterceptor { chain ->
|
|
||||||
val req = chain.request()
|
|
||||||
val response = chain.proceed(req)
|
|
||||||
val sourceId = sourceIdProducer()
|
|
||||||
findAndApplyPatches(sourceId)(req, response, sourceId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findAndApplyPatches(sourceId: Long): EHInterceptor {
|
|
||||||
// TODO make it so captcha doesnt auto open in manga eden while applying universal interceptors
|
|
||||||
return if (Injekt.get<UnsortedPreferences>().autoSolveCaptcha().get()) {
|
|
||||||
(EH_INTERCEPTORS[sourceId].orEmpty() + EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR].orEmpty()).merge()
|
|
||||||
} else {
|
|
||||||
EH_INTERCEPTORS[sourceId].orEmpty().merge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun List<EHInterceptor>.merge(): EHInterceptor {
|
|
||||||
return { request, response, sourceId ->
|
|
||||||
fold(response) { acc, int ->
|
|
||||||
int(request, acc, sourceId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val EH_UNIVERSAL_INTERCEPTOR = -1L
|
|
||||||
private val EH_INTERCEPTORS: Map<Long, List<EHInterceptor>> = mapOf(
|
|
||||||
EH_UNIVERSAL_INTERCEPTOR to listOf(
|
|
||||||
CAPTCHA_DETECTION_PATCH, // Auto captcha detection
|
|
||||||
),
|
|
||||||
)
|
|
@ -1,25 +0,0 @@
|
|||||||
package exh.patch
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import exh.ui.captcha.BrowserActionActivity
|
|
||||||
import exh.util.interceptAsHtml
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
val CAPTCHA_DETECTION_PATCH: EHInterceptor = { request, response, sourceId ->
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
response.interceptAsHtml { doc ->
|
|
||||||
// Find captcha
|
|
||||||
if (doc.getElementsByClass("g-recaptcha").isNotEmpty() || doc.getElementsByClass("h-captcha").isNotEmpty()) {
|
|
||||||
// Found it, allow the user to solve this thing
|
|
||||||
BrowserActionActivity.launchUniversal(
|
|
||||||
Injekt.get<Application>(),
|
|
||||||
sourceId,
|
|
||||||
request.url.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package exh.ui.captcha
|
|
||||||
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebView
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import org.jsoup.nodes.DataNode
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
class AutoSolvingWebViewClient(
|
|
||||||
activity: BrowserActionActivity,
|
|
||||||
verifyComplete: (String) -> Boolean,
|
|
||||||
injectScript: String?,
|
|
||||||
headers: Map<String, String>,
|
|
||||||
) : HeadersInjectingWebViewClient(activity, verifyComplete, injectScript, headers) {
|
|
||||||
|
|
||||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
|
||||||
// Inject our custom script into the recaptcha iframes
|
|
||||||
val lastPathSegment = request.url.pathSegments.lastOrNull()
|
|
||||||
if (lastPathSegment == "anchor" || lastPathSegment == "bframe") {
|
|
||||||
val oReq = request.toOkHttpRequest()
|
|
||||||
val response = activity.httpClient.newCall(oReq).execute()
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
doc.body().appendChild(Element("script").appendChild(DataNode(BrowserActionActivity.CROSS_WINDOW_SCRIPT_INNER)))
|
|
||||||
return WebResourceResponse(
|
|
||||||
"text/html",
|
|
||||||
"UTF-8",
|
|
||||||
doc.toString().byteInputStream().buffered(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package exh.ui.captcha
|
|
||||||
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
|
|
||||||
open class BasicWebViewClient(
|
|
||||||
protected val activity: BrowserActionActivity,
|
|
||||||
protected val verifyComplete: (String) -> Boolean,
|
|
||||||
private val injectScript: String?,
|
|
||||||
) : WebViewClient() {
|
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
|
||||||
super.onPageFinished(view, url)
|
|
||||||
|
|
||||||
if (verifyComplete(url)) {
|
|
||||||
activity.finish()
|
|
||||||
} else {
|
|
||||||
if (injectScript != null) {
|
|
||||||
view.evaluateJavascript("(function() {$injectScript})();", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,774 +0,0 @@
|
|||||||
package exh.ui.captcha
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.webkit.CookieManager
|
|
||||||
import android.webkit.JavascriptInterface
|
|
||||||
import android.webkit.JsResult
|
|
||||||
import android.webkit.WebChromeClient
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import eu.kanade.domain.UnsortedPreferences
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.databinding.EhActivityCaptchaBinding
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
|
||||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
|
||||||
import exh.log.xLogD
|
|
||||||
import exh.log.xLogE
|
|
||||||
import exh.source.DelegatedHttpSource
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.MultipartBody
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.Serializable
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class BrowserActionActivity : AppCompatActivity() {
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
private val preferencesHelper: UnsortedPreferences by injectLazy()
|
|
||||||
private val networkHelper: NetworkHelper by injectLazy()
|
|
||||||
|
|
||||||
val httpClient = networkHelper.client
|
|
||||||
|
|
||||||
private var currentLoopId: String? = null
|
|
||||||
private var validateCurrentLoopId: String? = null
|
|
||||||
private var strictValidationStartTime: Long? = null
|
|
||||||
|
|
||||||
private val credentialsFlow = MutableSharedFlow<String>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
|
||||||
private lateinit var binding: EhActivityCaptchaBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
binding = EhActivityCaptchaBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
|
|
||||||
val originalSource = if (sourceId != -1L) sourceManager.get(sourceId) else null
|
|
||||||
val source = if (originalSource != null) {
|
|
||||||
originalSource as? ActionCompletionVerifier
|
|
||||||
?: run {
|
|
||||||
(originalSource as? HttpSource)?.let {
|
|
||||||
NoopActionCompletionVerifier(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val headers = (
|
|
||||||
(source as? HttpSource)?.headers?.toMultimap()?.mapValues {
|
|
||||||
it.value.joinToString(",")
|
|
||||||
} ?: emptyMap()
|
|
||||||
) + (intent.getSerializableExtraCompat<HashMap<String, String>>(HEADERS_EXTRA) ?: emptyMap())
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val cookies = intent.getSerializableExtraCompat<HashMap<String, String>>(COOKIES_EXTRA)
|
|
||||||
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
|
||||||
val url: String? = intent.getStringExtra(URL_EXTRA)
|
|
||||||
val actionName = intent.getStringExtra(ACTION_NAME_EXTRA)
|
|
||||||
|
|
||||||
@Suppress("NOT_NULL_ASSERTION_ON_CALLABLE_REFERENCE", "UNCHECKED_CAST")
|
|
||||||
val verifyComplete: ((String) -> Boolean)? = if (source != null) {
|
|
||||||
source::verifyComplete!!
|
|
||||||
} else {
|
|
||||||
intent.getSerializableExtraCompat(VERIFY_LAMBDA_EXTRA)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verifyComplete == null || url == null) {
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val actionStr = actionName ?: "Solve captcha"
|
|
||||||
|
|
||||||
binding.toolbar.title = if (source != null) {
|
|
||||||
"${source.name}: $actionStr"
|
|
||||||
} else {
|
|
||||||
actionStr
|
|
||||||
}
|
|
||||||
|
|
||||||
val parsedUrl = URL(url)
|
|
||||||
|
|
||||||
val cm = CookieManager.getInstance()
|
|
||||||
|
|
||||||
cookies?.forEach { (t, u) ->
|
|
||||||
val cookieString = t + "=" + u + "; domain=" + parsedUrl.host
|
|
||||||
cm.setCookie(url, cookieString)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.webview.setDefaultSettings()
|
|
||||||
headers.entries.find { it.key.equals("user-agent", true) }?.let {
|
|
||||||
binding.webview.settings.userAgentString = it.value
|
|
||||||
}
|
|
||||||
|
|
||||||
var loadedInners = 0
|
|
||||||
|
|
||||||
binding.webview.webChromeClient = object : WebChromeClient() {
|
|
||||||
override fun onJsAlert(view: WebView?, url: String?, message: String, result: JsResult): Boolean {
|
|
||||||
if (message.startsWith("exh-")) {
|
|
||||||
loadedInners++
|
|
||||||
// Wait for both inner scripts to be loaded
|
|
||||||
if (loadedInners >= 2) {
|
|
||||||
// Attempt to autosolve captcha
|
|
||||||
if (preferencesHelper.autoSolveCaptcha().get()) {
|
|
||||||
binding.webview.post {
|
|
||||||
// 10 seconds to auto-solve captcha
|
|
||||||
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
|
|
||||||
beginSolveLoop()
|
|
||||||
beginValidateCaptchaLoop()
|
|
||||||
binding.webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE) {
|
|
||||||
binding.webview.evaluateJavascript(SOLVE_UI_SCRIPT_SHOW, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.confirm()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.webview.webViewClient = if (actionName == null && preferencesHelper.autoSolveCaptcha().get()) {
|
|
||||||
// Fetch auto-solve credentials early for speed
|
|
||||||
lifecycleScope.launchIO {
|
|
||||||
try {
|
|
||||||
credentialsFlow.emit(
|
|
||||||
httpClient.newCall(
|
|
||||||
Request.Builder()
|
|
||||||
// Rob demo credentials
|
|
||||||
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
|
|
||||||
.build(),
|
|
||||||
).await().parseAs<JsonObject>()["token"]!!.jsonPrimitive.content,
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
xLogE("Failed to get credentials", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.webview.addJavascriptInterface(this@BrowserActionActivity, "exh")
|
|
||||||
AutoSolvingWebViewClient(this, verifyComplete, script, headers)
|
|
||||||
} else {
|
|
||||||
HeadersInjectingWebViewClient(this, verifyComplete, script, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.webview.loadUrl(url, headers)
|
|
||||||
|
|
||||||
setSupportActionBar(binding.toolbar)
|
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun captchaSolveFail() {
|
|
||||||
currentLoopId = null
|
|
||||||
validateCurrentLoopId = null
|
|
||||||
xLogE("Captcha solve Error", IllegalStateException("Captcha solve failure!"))
|
|
||||||
withUIContext {
|
|
||||||
binding.webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
|
||||||
MaterialAlertDialogBuilder(this@BrowserActionActivity)
|
|
||||||
.setTitle(R.string.captcha_solve_failure)
|
|
||||||
.setMessage(R.string.captcha_solve_failure_message)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JavascriptInterface
|
|
||||||
suspend fun callback(result: String?, loopId: String, stage: Int) {
|
|
||||||
if (loopId != currentLoopId) return
|
|
||||||
|
|
||||||
when (stage) {
|
|
||||||
STAGE_CHECKBOX -> {
|
|
||||||
if (result!!.toBoolean()) {
|
|
||||||
binding.webview.postDelayed(
|
|
||||||
{
|
|
||||||
getAudioButtonLocation(loopId)
|
|
||||||
},
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
binding.webview.postDelayed(
|
|
||||||
{
|
|
||||||
doStageCheckbox(loopId)
|
|
||||||
},
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
STAGE_GET_AUDIO_BTN_LOCATION -> {
|
|
||||||
if (result != null) {
|
|
||||||
val splitResult = result.split(" ").map { it.toFloat() }
|
|
||||||
val origX = splitResult[0]
|
|
||||||
val origY = splitResult[1]
|
|
||||||
val iw = splitResult[2]
|
|
||||||
val ih = splitResult[3]
|
|
||||||
val x = binding.webview.x + origX / iw * binding.webview.width
|
|
||||||
val y = binding.webview.y + origY / ih * binding.webview.height
|
|
||||||
xLogD("Found audio button coords: %f %f", x, y)
|
|
||||||
simulateClick(x + 50, y + 50)
|
|
||||||
binding.webview.post {
|
|
||||||
doStageDownloadAudio(loopId)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.webview.postDelayed(
|
|
||||||
{
|
|
||||||
getAudioButtonLocation(loopId)
|
|
||||||
},
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
STAGE_DOWNLOAD_AUDIO -> {
|
|
||||||
if (result != null) {
|
|
||||||
xLogD("Got audio URL: $result")
|
|
||||||
lifecycleScope.launchIO {
|
|
||||||
try {
|
|
||||||
val transcript = performRecognize(result)
|
|
||||||
xLogD("Got audio transcript: $transcript")
|
|
||||||
binding.webview.post {
|
|
||||||
typeResult(
|
|
||||||
loopId,
|
|
||||||
transcript
|
|
||||||
.replace(TRANSCRIPT_CLEANER_REGEX, "")
|
|
||||||
.replace(SPACE_DEDUPE_REGEX, " ")
|
|
||||||
.trim(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
captchaSolveFail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.webview.postDelayed(
|
|
||||||
{
|
|
||||||
doStageDownloadAudio(loopId)
|
|
||||||
},
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
STAGE_TYPE_RESULT -> {
|
|
||||||
if (result!!.toBoolean()) {
|
|
||||||
// Fail if captcha still not solved after 1.5s
|
|
||||||
strictValidationStartTime = System.currentTimeMillis() + 1500
|
|
||||||
} else {
|
|
||||||
captchaSolveFail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun performRecognize(url: String): String {
|
|
||||||
val token = credentialsFlow.first()
|
|
||||||
val audioFile = httpClient.newCall(
|
|
||||||
Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.build(),
|
|
||||||
).await().body.bytes()
|
|
||||||
val response = httpClient.newCall(
|
|
||||||
POST(
|
|
||||||
"https://stream.watsonplatform.net/speech-to-text/api/v1/recognize".toHttpUrl()
|
|
||||||
.newBuilder()
|
|
||||||
.addQueryParameter("watson-token", token)
|
|
||||||
.build()
|
|
||||||
.toString(),
|
|
||||||
body = MultipartBody.Builder()
|
|
||||||
.setType(MultipartBody.FORM)
|
|
||||||
.addFormDataPart("jsonDescription", RECOGNIZE_JSON)
|
|
||||||
.addFormDataPart(
|
|
||||||
"audio.mp3",
|
|
||||||
"audio.mp3",
|
|
||||||
audioFile.toRequestBody(
|
|
||||||
"audio/mp3".toMediaTypeOrNull(),
|
|
||||||
0,
|
|
||||||
audioFile.size,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.build(),
|
|
||||||
),
|
|
||||||
).await()
|
|
||||||
return response.parseAs<JsonObject>()["results"]!!
|
|
||||||
.jsonArray[0]
|
|
||||||
.jsonObject["alternatives"]!!
|
|
||||||
.jsonArray[0]
|
|
||||||
.jsonObject["transcript"]!!
|
|
||||||
.jsonPrimitive
|
|
||||||
.content
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doStageCheckbox(loopId: String) {
|
|
||||||
if (loopId != currentLoopId) return
|
|
||||||
|
|
||||||
binding.webview.evaluateJavascript(
|
|
||||||
"""
|
|
||||||
(function() {
|
|
||||||
$CROSS_WINDOW_SCRIPT_OUTER
|
|
||||||
|
|
||||||
let exh_cframe = document.querySelector('iframe[role=presentation][name|=a]');
|
|
||||||
|
|
||||||
if(exh_cframe != null) {
|
|
||||||
cwmExec(exh_cframe, `
|
|
||||||
let exh_cb = document.getElementsByClassName('recaptcha-checkbox-checkmark')[0];
|
|
||||||
if(exh_cb != null) {
|
|
||||||
exh_cb.click();
|
|
||||||
return "true";
|
|
||||||
} else {
|
|
||||||
return "false";
|
|
||||||
}
|
|
||||||
`, function(result) {
|
|
||||||
exh.callback(result, '$loopId', $STAGE_CHECKBOX);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
exh.callback("false", '$loopId', $STAGE_CHECKBOX);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
""".trimIndent().replace("\n", ""),
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAudioButtonLocation(loopId: String) {
|
|
||||||
binding.webview.evaluateJavascript(
|
|
||||||
"""
|
|
||||||
(function() {
|
|
||||||
$CROSS_WINDOW_SCRIPT_OUTER
|
|
||||||
|
|
||||||
let exh_bframe = document.querySelector("iframe[title='recaptcha challenge'][name|=c]");
|
|
||||||
|
|
||||||
if(exh_bframe != null) {
|
|
||||||
let bfb = exh_bframe.getBoundingClientRect();
|
|
||||||
let iw = window.innerWidth;
|
|
||||||
let ih = window.innerHeight;
|
|
||||||
if(bfb.left < 0 || bfb.top < 0) {
|
|
||||||
exh.callback(null, '$loopId', $STAGE_GET_AUDIO_BTN_LOCATION);
|
|
||||||
} else {
|
|
||||||
cwmExec(exh_bframe, ` let exh_ab = document.getElementById("recaptcha-audio-button");
|
|
||||||
if(exh_ab != null) {
|
|
||||||
let bounds = exh_ab.getBoundingClientRect();
|
|
||||||
return (${'$'}{bfb.left} + bounds.left) + " " + (${'$'}{bfb.top} + bounds.top) + " " + ${'$'}{iw} + " " + ${'$'}{ih};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
`, function(result) {
|
|
||||||
exh.callback(result, '$loopId', $STAGE_GET_AUDIO_BTN_LOCATION);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
exh.callback(null, '$loopId', $STAGE_GET_AUDIO_BTN_LOCATION);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
""".trimIndent().replace("\n", ""),
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doStageDownloadAudio(loopId: String) {
|
|
||||||
binding.webview.evaluateJavascript(
|
|
||||||
"""
|
|
||||||
(function() {
|
|
||||||
$CROSS_WINDOW_SCRIPT_OUTER
|
|
||||||
|
|
||||||
let exh_bframe = document.querySelector("iframe[title='recaptcha challenge'][name|=c]");
|
|
||||||
|
|
||||||
if(exh_bframe != null) {
|
|
||||||
cwmExec(exh_bframe, `
|
|
||||||
let exh_as = document.getElementById("audio-source");
|
|
||||||
if(exh_as != null) {
|
|
||||||
return exh_as.src;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
`, function(result) {
|
|
||||||
exh.callback(result, '$loopId', $STAGE_DOWNLOAD_AUDIO);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
exh.callback(null, '$loopId', $STAGE_DOWNLOAD_AUDIO);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
""".trimIndent().replace("\n", ""),
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun typeResult(loopId: String, result: String) {
|
|
||||||
binding.webview.evaluateJavascript(
|
|
||||||
"""
|
|
||||||
(function() {
|
|
||||||
$CROSS_WINDOW_SCRIPT_OUTER
|
|
||||||
|
|
||||||
let exh_bframe = document.querySelector("iframe[title='recaptcha challenge'][name|=c]");
|
|
||||||
|
|
||||||
if(exh_bframe != null) {
|
|
||||||
cwmExec(exh_bframe, `
|
|
||||||
let exh_as = document.getElementById("audio-response");
|
|
||||||
let exh_vb = document.getElementById("recaptcha-verify-button");
|
|
||||||
if(exh_as != null && exh_vb != null) {
|
|
||||||
exh_as.value = "$result";
|
|
||||||
exh_vb.click();
|
|
||||||
return "true";
|
|
||||||
} else {
|
|
||||||
return "false";
|
|
||||||
}
|
|
||||||
`, function(result) {
|
|
||||||
exh.callback(result, '$loopId', $STAGE_TYPE_RESULT);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
exh.callback("false", '$loopId', $STAGE_TYPE_RESULT);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
""".trimIndent().replace("\n", ""),
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun beginSolveLoop() {
|
|
||||||
val loopId = UUID.randomUUID().toString()
|
|
||||||
currentLoopId = loopId
|
|
||||||
doStageCheckbox(loopId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JavascriptInterface
|
|
||||||
suspend fun validateCaptchaCallback(result: Boolean, loopId: String) {
|
|
||||||
if (loopId != validateCurrentLoopId) return
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
xLogD("Captcha solved!")
|
|
||||||
binding.webview.post {
|
|
||||||
binding.webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
|
||||||
}
|
|
||||||
val asbtn = intent.getStringExtra(ASBTN_EXTRA)
|
|
||||||
if (asbtn != null) {
|
|
||||||
binding.webview.post {
|
|
||||||
binding.webview.evaluateJavascript("(function() {document.querySelector('$asbtn').click();})();", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val savedStrictValidationStartTime = strictValidationStartTime
|
|
||||||
if (savedStrictValidationStartTime != null &&
|
|
||||||
System.currentTimeMillis() > savedStrictValidationStartTime
|
|
||||||
) {
|
|
||||||
captchaSolveFail()
|
|
||||||
} else {
|
|
||||||
binding.webview.postDelayed(
|
|
||||||
{
|
|
||||||
runValidateCaptcha(loopId)
|
|
||||||
},
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runValidateCaptcha(loopId: String) {
|
|
||||||
if (loopId != validateCurrentLoopId) return
|
|
||||||
|
|
||||||
binding.webview.evaluateJavascript(
|
|
||||||
"""
|
|
||||||
(function() {
|
|
||||||
$CROSS_WINDOW_SCRIPT_OUTER
|
|
||||||
|
|
||||||
let exh_cframe = document.querySelector('iframe[role=presentation][name|=a]');
|
|
||||||
|
|
||||||
if(exh_cframe != null) {
|
|
||||||
cwmExec(exh_cframe, `
|
|
||||||
let exh_cb = document.querySelector(".recaptcha-checkbox[aria-checked=true]");
|
|
||||||
if(exh_cb != null) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
`, function(result) {
|
|
||||||
exh.validateCaptchaCallback(result, '$loopId');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
exh.validateCaptchaCallback(false, '$loopId');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
""".trimIndent().replace("\n", ""),
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun beginValidateCaptchaLoop() {
|
|
||||||
val loopId = UUID.randomUUID().toString()
|
|
||||||
validateCurrentLoopId = loopId
|
|
||||||
runValidateCaptcha(loopId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun simulateClick(x: Float, y: Float) {
|
|
||||||
val downTime = SystemClock.uptimeMillis()
|
|
||||||
val eventTime = SystemClock.uptimeMillis()
|
|
||||||
val properties = arrayOfNulls<MotionEvent.PointerProperties>(1)
|
|
||||||
val pp1 = MotionEvent.PointerProperties().apply {
|
|
||||||
id = 0
|
|
||||||
toolType = MotionEvent.TOOL_TYPE_FINGER
|
|
||||||
}
|
|
||||||
properties[0] = pp1
|
|
||||||
val pointerCoords = arrayOfNulls<MotionEvent.PointerCoords>(1)
|
|
||||||
val pc1 = MotionEvent.PointerCoords().apply {
|
|
||||||
this.x = x
|
|
||||||
this.y = y
|
|
||||||
pressure = 1f
|
|
||||||
size = 1f
|
|
||||||
}
|
|
||||||
pointerCoords[0] = pc1
|
|
||||||
var motionEvent = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, 1, properties, pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
|
|
||||||
dispatchTouchEvent(motionEvent)
|
|
||||||
motionEvent.recycle()
|
|
||||||
motionEvent = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, 1, properties, pointerCoords, 0, 0, 1f, 1f, 0, 0, 0, 0)
|
|
||||||
dispatchTouchEvent(motionEvent)
|
|
||||||
motionEvent.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val VERIFY_LAMBDA_EXTRA = "verify_lambda_extra"
|
|
||||||
const val SOURCE_ID_EXTRA = "source_id_extra"
|
|
||||||
const val COOKIES_EXTRA = "cookies_extra"
|
|
||||||
const val SCRIPT_EXTRA = "script_extra"
|
|
||||||
const val URL_EXTRA = "url_extra"
|
|
||||||
const val ASBTN_EXTRA = "asbtn_extra"
|
|
||||||
const val ACTION_NAME_EXTRA = "action_name_extra"
|
|
||||||
const val HEADERS_EXTRA = "headers_extra"
|
|
||||||
|
|
||||||
const val STAGE_CHECKBOX = 0
|
|
||||||
const val STAGE_GET_AUDIO_BTN_LOCATION = 1
|
|
||||||
const val STAGE_DOWNLOAD_AUDIO = 2
|
|
||||||
const val STAGE_TYPE_RESULT = 3
|
|
||||||
|
|
||||||
val CROSS_WINDOW_SCRIPT_OUTER =
|
|
||||||
"""
|
|
||||||
function cwmExec(element, code, cb) {
|
|
||||||
console.log(">>> [CWM-Outer] Running: " + code);
|
|
||||||
let runId = Math.random();
|
|
||||||
if(cb != null) {
|
|
||||||
let listener;
|
|
||||||
listener = function(event) {
|
|
||||||
if(typeof event.data === "string" && event.data.startsWith("exh-")) {
|
|
||||||
let response = JSON.parse(event.data.substring(4));
|
|
||||||
if(response.id === runId) {
|
|
||||||
cb(response.result);
|
|
||||||
window.removeEventListener('message', listener);
|
|
||||||
console.log(">>> [CWM-Outer] Finished: " + response.id + " ==> " + response.result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('message', listener, false);
|
|
||||||
}
|
|
||||||
let runRequest = { id: runId, code: code };
|
|
||||||
element.contentWindow.postMessage("exh-" + JSON.stringify(runRequest), "*");
|
|
||||||
}
|
|
||||||
""".trimIndent().replace("\n", "")
|
|
||||||
|
|
||||||
val CROSS_WINDOW_SCRIPT_INNER =
|
|
||||||
"""
|
|
||||||
window.addEventListener('message', function(event) {
|
|
||||||
if(typeof event.data === "string" && event.data.startsWith("exh-")) {
|
|
||||||
let request = JSON.parse(event.data.substring(4));
|
|
||||||
console.log(">>> [CWM-Inner] Incoming: " + request.id);
|
|
||||||
let result = eval("(function() {" + request.code + "})();");
|
|
||||||
let response = { id: request.id, result: result };
|
|
||||||
console.log(">>> [CWM-Inner] Outgoing: " + response.id + " ==> " + response.result);
|
|
||||||
event.source.postMessage("exh-" + JSON.stringify(response), event.origin);
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
console.log(">>> [CWM-Inner] Loaded!");
|
|
||||||
alert("exh-");
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val SOLVE_UI_SCRIPT_SHOW =
|
|
||||||
"""
|
|
||||||
(function() {
|
|
||||||
let exh_overlay = document.createElement("div");
|
|
||||||
exh_overlay.id = "exh_overlay";
|
|
||||||
exh_overlay.style.zIndex = 2000000001;
|
|
||||||
exh_overlay.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
|
|
||||||
exh_overlay.style.position = "fixed";
|
|
||||||
exh_overlay.style.top = 0;
|
|
||||||
exh_overlay.style.left = 0;
|
|
||||||
exh_overlay.style.width = "100%";
|
|
||||||
exh_overlay.style.height = "100%";
|
|
||||||
exh_overlay.style.pointerEvents = "none";
|
|
||||||
document.body.appendChild(exh_overlay);
|
|
||||||
let exh_otext = document.createElement("div");
|
|
||||||
exh_otext.id = "exh_otext";
|
|
||||||
exh_otext.style.zIndex = 2000000002;
|
|
||||||
exh_otext.style.position = "fixed";
|
|
||||||
exh_otext.style.top = "50%";
|
|
||||||
exh_otext.style.left = 0;
|
|
||||||
exh_otext.style.transform = "translateY(-50%)";
|
|
||||||
exh_otext.style.color = "white";
|
|
||||||
exh_otext.style.fontSize = "25pt";
|
|
||||||
exh_otext.style.pointerEvents = "none";
|
|
||||||
exh_otext.style.width = "100%";
|
|
||||||
exh_otext.style.textAlign = "center";
|
|
||||||
exh_otext.textContent = "Solving captcha..."
|
|
||||||
document.body.appendChild(exh_otext);
|
|
||||||
})();
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val SOLVE_UI_SCRIPT_HIDE =
|
|
||||||
"""
|
|
||||||
(function() {
|
|
||||||
let exh_overlay = document.getElementById("exh_overlay");
|
|
||||||
let exh_otext = document.getElementById("exh_otext");
|
|
||||||
if(exh_overlay != null) exh_overlay.remove();
|
|
||||||
if(exh_otext != null) exh_otext.remove();
|
|
||||||
})();
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val RECOGNIZE_JSON =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"part_content_type": "audio/mp3",
|
|
||||||
"keywords": [],
|
|
||||||
"profanity_filter": false,
|
|
||||||
"max_alternatives": 1,
|
|
||||||
"speaker_labels": false,
|
|
||||||
"firstReadyInSession": false,
|
|
||||||
"preserveAdaptation": false,
|
|
||||||
"timestamps": false,
|
|
||||||
"inactivity_timeout": 30,
|
|
||||||
"word_confidence": false,
|
|
||||||
"audioMetrics": false,
|
|
||||||
"latticeGeneration": true,
|
|
||||||
"customGrammarWords": [],
|
|
||||||
"action": "recognize"
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val TRANSCRIPT_CLEANER_REGEX = Regex("[^0-9a-zA-Z_ -]")
|
|
||||||
val SPACE_DEDUPE_REGEX = Regex(" +")
|
|
||||||
|
|
||||||
private fun baseIntent(context: Context) =
|
|
||||||
Intent(context, BrowserActionActivity::class.java).apply {
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchCaptcha(
|
|
||||||
context: Context,
|
|
||||||
source: ActionCompletionVerifier,
|
|
||||||
cookies: Map<String, String>,
|
|
||||||
script: String?,
|
|
||||||
url: String,
|
|
||||||
autoSolveSubmitBtnSelector: String? = null,
|
|
||||||
) {
|
|
||||||
val intent = baseIntent(context).apply {
|
|
||||||
putExtra(SOURCE_ID_EXTRA, source.id)
|
|
||||||
putExtra(COOKIES_EXTRA, HashMap(cookies))
|
|
||||||
putExtra(SCRIPT_EXTRA, script)
|
|
||||||
putExtra(URL_EXTRA, url)
|
|
||||||
putExtra(ASBTN_EXTRA, autoSolveSubmitBtnSelector)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchUniversal(
|
|
||||||
context: Context,
|
|
||||||
source: HttpSource,
|
|
||||||
url: String,
|
|
||||||
) {
|
|
||||||
val intent = baseIntent(context).apply {
|
|
||||||
putExtra(SOURCE_ID_EXTRA, source.id)
|
|
||||||
putExtra(URL_EXTRA, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchUniversal(
|
|
||||||
context: Context,
|
|
||||||
sourceId: Long,
|
|
||||||
url: String,
|
|
||||||
) {
|
|
||||||
val intent = baseIntent(context).apply {
|
|
||||||
putExtra(SOURCE_ID_EXTRA, sourceId)
|
|
||||||
putExtra(URL_EXTRA, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchAction(
|
|
||||||
context: Context,
|
|
||||||
completionVerifier: ActionCompletionVerifier,
|
|
||||||
script: String?,
|
|
||||||
url: String,
|
|
||||||
actionName: String,
|
|
||||||
) {
|
|
||||||
val intent = baseIntent(context).apply {
|
|
||||||
putExtra(SOURCE_ID_EXTRA, completionVerifier.id)
|
|
||||||
putExtra(SCRIPT_EXTRA, script)
|
|
||||||
putExtra(URL_EXTRA, url)
|
|
||||||
putExtra(ACTION_NAME_EXTRA, actionName)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launchAction(
|
|
||||||
context: Context,
|
|
||||||
completionVerifier: (String) -> Boolean,
|
|
||||||
script: String?,
|
|
||||||
url: String,
|
|
||||||
actionName: String,
|
|
||||||
headers: Map<String, String>? = emptyMap(),
|
|
||||||
) {
|
|
||||||
val intent = baseIntent(context).apply {
|
|
||||||
putExtra(HEADERS_EXTRA, HashMap(headers!!))
|
|
||||||
putExtra(VERIFY_LAMBDA_EXTRA, completionVerifier as Serializable)
|
|
||||||
putExtra(SCRIPT_EXTRA, script)
|
|
||||||
putExtra(URL_EXTRA, url)
|
|
||||||
putExtra(ACTION_NAME_EXTRA, actionName)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoopActionCompletionVerifier(private val source: HttpSource) :
|
|
||||||
DelegatedHttpSource(source),
|
|
||||||
ActionCompletionVerifier {
|
|
||||||
override val versionId get() = source.versionId
|
|
||||||
override val lang: String get() = source.lang
|
|
||||||
|
|
||||||
override fun verifyComplete(url: String) = false
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActionCompletionVerifier : Source {
|
|
||||||
fun verifyComplete(url: String): Boolean
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package exh.ui.captcha
|
|
||||||
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebView
|
|
||||||
|
|
||||||
open class HeadersInjectingWebViewClient(
|
|
||||||
activity: BrowserActionActivity,
|
|
||||||
verifyComplete: (String) -> Boolean,
|
|
||||||
injectScript: String?,
|
|
||||||
private val headers: Map<String, String>,
|
|
||||||
) : BasicWebViewClient(activity, verifyComplete, injectScript) {
|
|
||||||
|
|
||||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
|
||||||
// Temp disabled as it's unreliable
|
|
||||||
/*if(headers.isNotEmpty()) {
|
|
||||||
val response = activity.httpClient.newCall(request.toOkHttpRequest()
|
|
||||||
.newBuilder()
|
|
||||||
.apply {
|
|
||||||
headers.forEach { (n, v) -> header(n, v) }
|
|
||||||
}
|
|
||||||
.build())
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
return WebResourceResponse(
|
|
||||||
response.body()?.contentType()?.let { "${it.type()}/${it.subtype()}" },
|
|
||||||
response.body()?.contentType()?.charset()?.toString(),
|
|
||||||
response.code(),
|
|
||||||
response.message().nullIfBlank() ?: FALLBACK_REASON_PHRASES[response.code()] ?: "Unknown status",
|
|
||||||
response.headers().toMultimap().mapValues { it.value.joinToString(",") },
|
|
||||||
response.body()?.byteStream()
|
|
||||||
)
|
|
||||||
}*/
|
|
||||||
return super.shouldInterceptRequest(view, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val FALLBACK_REASON_PHRASES = mapOf(
|
|
||||||
100 to "Continue",
|
|
||||||
101 to "Switching Protocols",
|
|
||||||
200 to "OK",
|
|
||||||
201 to "Created",
|
|
||||||
202 to "Accepted",
|
|
||||||
203 to "Non-Authoritative Information",
|
|
||||||
204 to "No Content",
|
|
||||||
205 to "Reset Content",
|
|
||||||
206 to "Partial Content",
|
|
||||||
300 to "Multiple Choices",
|
|
||||||
301 to "Moved Permanently",
|
|
||||||
302 to "Moved Temporarily",
|
|
||||||
303 to "See Other",
|
|
||||||
304 to "Not Modified",
|
|
||||||
305 to "Use Proxy",
|
|
||||||
400 to "Bad Request",
|
|
||||||
401 to "Unauthorized",
|
|
||||||
402 to "Payment Required",
|
|
||||||
403 to "Forbidden",
|
|
||||||
404 to "Not Found",
|
|
||||||
405 to "Method Not Allowed",
|
|
||||||
406 to "Not Acceptable",
|
|
||||||
407 to "Proxy Authentication Required",
|
|
||||||
408 to "Request Time-out",
|
|
||||||
409 to "Conflict",
|
|
||||||
410 to "Gone",
|
|
||||||
411 to "Length Required",
|
|
||||||
412 to "Precondition Failed",
|
|
||||||
413 to "Request Entity Too Large",
|
|
||||||
414 to "Request-URI Too Large",
|
|
||||||
415 to "Unsupported Media Type",
|
|
||||||
500 to "Internal Server Error",
|
|
||||||
501 to "Not Implemented",
|
|
||||||
502 to "Bad Gateway",
|
|
||||||
503 to "Service Unavailable",
|
|
||||||
504 to "Gateway Time-out",
|
|
||||||
505 to "HTTP Version not supported",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package exh.ui.captcha
|
|
||||||
|
|
||||||
import android.webkit.WebResourceRequest
|
|
||||||
import okhttp3.Request
|
|
||||||
|
|
||||||
fun WebResourceRequest.toOkHttpRequest(): Request {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url.toString())
|
|
||||||
.method(method, null)
|
|
||||||
|
|
||||||
requestHeaders.entries.forEach { (t, u) ->
|
|
||||||
request.addHeader(t, u)
|
|
||||||
}
|
|
||||||
|
|
||||||
return request.build()
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.TachiyomiAppBarLayout
|
|
||||||
android:id="@+id/appbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
app:elevation="0dp">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
android:theme="?attr/actionBarTheme" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.TachiyomiAppBarLayout>
|
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:id="@+id/webview"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_below="@+id/appbar"
|
|
||||||
android:layout_centerHorizontal="true" />
|
|
||||||
</RelativeLayout>
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
Loading…
x
Reference in New Issue
Block a user