Remove auto-solve captcha

This commit is contained in:
Jobobby04 2022-12-03 13:54:14 -05:00
parent 726626f2c5
commit 0784629cbb
11 changed files with 0 additions and 1123 deletions

View File

@ -381,10 +381,6 @@
<data android:pathPattern="/chapter/..*" />
</intent-filter>
</activity>
<activity
android:name="exh.ui.captcha.BrowserActionActivity"
android:theme="@style/Theme.Tachiyomi"
android:exported="false"/>
</application>
</manifest>

View File

@ -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<LibraryPreferences>() }
// SY -->
val uiPrefs = remember { Injekt.get<UiPreferences>() }
val unsortedPrefs = remember { Injekt.get<UnsortedPreferences>() }
// SY <--
return mutableListOf<Preference>().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),

View File

@ -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"

View File

@ -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
),
)

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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
}

View File

@ -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",
)
}
}

View File

@ -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()
}

View File

@ -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>