diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 767a6d4a7..e087f9843 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -381,10 +381,6 @@
-
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsGeneralScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsGeneralScreen.kt
index f671662bb..fa85a2e87 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsGeneralScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsGeneralScreen.kt
@@ -16,7 +16,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.os.LocaleListCompat
-import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.domain.ui.UiPreferences
@@ -40,7 +39,6 @@ object SettingsGeneralScreen : SearchableSettings {
val libraryPrefs = remember { Injekt.get() }
// SY -->
val uiPrefs = remember { Injekt.get() }
- val unsortedPrefs = remember { Injekt.get() }
// SY <--
return mutableListOf().apply {
add(
@@ -104,11 +102,6 @@ object SettingsGeneralScreen : SearchableSettings {
pref = uiPrefs.expandFilters(),
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(
pref = uiPrefs.recommendsInOverflow(),
title = stringResource(R.string.put_recommends_in_overflow),
diff --git a/app/src/main/java/exh/patch/MangaDexLogin.kt b/app/src/main/java/exh/patch/MangaDexLogin.kt
deleted file mode 100644
index f1fc2bddc..000000000
--- a/app/src/main/java/exh/patch/MangaDexLogin.kt
+++ /dev/null
@@ -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(),
- ::verifyComplete,
- HIDE_SCRIPT,
- "https://mangadex.org/login",
- "Login",
- (Injekt.get().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"
diff --git a/app/src/main/java/exh/patch/NetworkPatches.kt b/app/src/main/java/exh/patch/NetworkPatches.kt
deleted file mode 100644
index ab5a5571e..000000000
--- a/app/src/main/java/exh/patch/NetworkPatches.kt
+++ /dev/null
@@ -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().autoSolveCaptcha().get()) {
- (EH_INTERCEPTORS[sourceId].orEmpty() + EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR].orEmpty()).merge()
- } else {
- EH_INTERCEPTORS[sourceId].orEmpty().merge()
- }
-}
-
-fun List.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> = mapOf(
- EH_UNIVERSAL_INTERCEPTOR to listOf(
- CAPTCHA_DETECTION_PATCH, // Auto captcha detection
- ),
-)
diff --git a/app/src/main/java/exh/patch/UniversalCaptchaDetection.kt b/app/src/main/java/exh/patch/UniversalCaptchaDetection.kt
deleted file mode 100644
index 16de4eab5..000000000
--- a/app/src/main/java/exh/patch/UniversalCaptchaDetection.kt
+++ /dev/null
@@ -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(),
- sourceId,
- request.url.toString(),
- )
- }
- }
- } else {
- response
- }
-}
diff --git a/app/src/main/java/exh/ui/captcha/AutoSolvingWebViewClient.kt b/app/src/main/java/exh/ui/captcha/AutoSolvingWebViewClient.kt
deleted file mode 100644
index 278d66aa3..000000000
--- a/app/src/main/java/exh/ui/captcha/AutoSolvingWebViewClient.kt
+++ /dev/null
@@ -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,
-) : 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)
- }
-}
diff --git a/app/src/main/java/exh/ui/captcha/BasicWebViewClient.kt b/app/src/main/java/exh/ui/captcha/BasicWebViewClient.kt
deleted file mode 100644
index 575efd647..000000000
--- a/app/src/main/java/exh/ui/captcha/BasicWebViewClient.kt
+++ /dev/null
@@ -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)
- }
- }
- }
-}
diff --git a/app/src/main/java/exh/ui/captcha/BrowserActionActivity.kt b/app/src/main/java/exh/ui/captcha/BrowserActionActivity.kt
deleted file mode 100644
index a588cae93..000000000
--- a/app/src/main/java/exh/ui/captcha/BrowserActionActivity.kt
+++ /dev/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(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>(HEADERS_EXTRA) ?: emptyMap())
-
- @Suppress("UNCHECKED_CAST")
- val cookies = intent.getSerializableExtraCompat>(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()["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()["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(1)
- val pp1 = MotionEvent.PointerProperties().apply {
- id = 0
- toolType = MotionEvent.TOOL_TYPE_FINGER
- }
- properties[0] = pp1
- val pointerCoords = arrayOfNulls(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,
- 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? = 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
-}
diff --git a/app/src/main/java/exh/ui/captcha/HeadersInjectingWebViewClient.kt b/app/src/main/java/exh/ui/captcha/HeadersInjectingWebViewClient.kt
deleted file mode 100644
index e6ec54cb0..000000000
--- a/app/src/main/java/exh/ui/captcha/HeadersInjectingWebViewClient.kt
+++ /dev/null
@@ -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,
-) : 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",
- )
- }
-}
diff --git a/app/src/main/java/exh/ui/captcha/WebViewUtil.kt b/app/src/main/java/exh/ui/captcha/WebViewUtil.kt
deleted file mode 100644
index fa4ce3693..000000000
--- a/app/src/main/java/exh/ui/captcha/WebViewUtil.kt
+++ /dev/null
@@ -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()
-}
diff --git a/app/src/main/res/layout/eh_activity_captcha.xml b/app/src/main/res/layout/eh_activity_captcha.xml
deleted file mode 100644
index 5649b0e6b..000000000
--- a/app/src/main/res/layout/eh_activity_captcha.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file