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