Fix EHentai/ExHentai Fix hitomi.la Fix hitomi.la crashing application Rewrite hitomi.la search engine to be faster, use less CPU and require no preloading Fix nhentai Add additional filters to nhentai Fix PervEden Introduce delegated sources Rewrite HentaiCafe to be a delegated source Introduce ability to save/load search presets Temporarily disable misbehaving native Tachiyomi migrations Fix tap-to-search-tag breaking on aliased tags Add debug menu Add experimental automatic captcha solver Add app name to wakelock names Add ability to interrupt metadata migrator Fix incognito open-in-browser being zoomed in immediately when it's opened
618 lines
24 KiB
Kotlin
618 lines
24 KiB
Kotlin
package exh.ui.captcha
|
|
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.support.annotation.RequiresApi
|
|
import android.support.v7.app.AppCompatActivity
|
|
import android.webkit.*
|
|
import com.github.salomonbrys.kotson.get
|
|
import com.github.salomonbrys.kotson.string
|
|
import com.google.gson.JsonParser
|
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
import eu.kanade.tachiyomi.source.Source
|
|
import eu.kanade.tachiyomi.source.SourceManager
|
|
import kotlinx.android.synthetic.main.eh_activity_captcha.*
|
|
import okhttp3.*
|
|
import rx.Single
|
|
import rx.schedulers.Schedulers
|
|
import timber.log.Timber
|
|
import uy.kohesive.injekt.injectLazy
|
|
import java.net.URL
|
|
import java.util.*
|
|
import android.view.MotionEvent
|
|
import android.os.SystemClock
|
|
import com.afollestad.materialdialogs.MaterialDialog
|
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
import exh.util.melt
|
|
import rx.Observable
|
|
|
|
class SolveCaptchaActivity : AppCompatActivity() {
|
|
private val sourceManager: SourceManager by injectLazy()
|
|
private val preferencesHelper: PreferencesHelper by injectLazy()
|
|
|
|
val httpClient = OkHttpClient()
|
|
private val jsonParser = JsonParser()
|
|
|
|
private var currentLoopId: String? = null
|
|
private var validateCurrentLoopId: String? = null
|
|
private var strictValidationStartTime: Long? = null
|
|
|
|
lateinit var credentialsObservable: Observable<String>
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
setContentView(eu.kanade.tachiyomi.R.layout.eh_activity_captcha)
|
|
|
|
val sourceId = intent.getLongExtra(SOURCE_ID_EXTRA, -1)
|
|
val source = if(sourceId != -1L)
|
|
sourceManager.get(sourceId) as? CaptchaCompletionVerifier
|
|
else null
|
|
|
|
val cookies: HashMap<String, String>?
|
|
= intent.getSerializableExtra(COOKIES_EXTRA) as? HashMap<String, String>
|
|
|
|
val script: String? = intent.getStringExtra(SCRIPT_EXTRA)
|
|
|
|
val url: String? = intent.getStringExtra(URL_EXTRA)
|
|
|
|
if(source == null || cookies == null || url == null) {
|
|
finish()
|
|
return
|
|
}
|
|
|
|
toolbar.title = source.name + ": Solve captcha"
|
|
|
|
val parsedUrl = URL(url)
|
|
|
|
val cm = CookieManager.getInstance()
|
|
|
|
fun continueLoading() {
|
|
cookies.forEach { (t, u) ->
|
|
val cookieString = t + "=" + u + "; domain=" + parsedUrl.host
|
|
cm.setCookie(url, cookieString)
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
|
CookieSyncManager.createInstance(this).sync()
|
|
|
|
webview.settings.javaScriptEnabled = true
|
|
webview.settings.domStorageEnabled = true
|
|
|
|
var loadedInners = 0
|
|
|
|
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.eh_autoSolveCaptchas().getOrDefault()
|
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
webview.post {
|
|
// 10 seconds to auto-solve captcha
|
|
strictValidationStartTime = System.currentTimeMillis() + 1000 * 10
|
|
beginSolveLoop()
|
|
beginValidateCaptchaLoop()
|
|
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE) {
|
|
webview.evaluateJavascript(SOLVE_UI_SCRIPT_SHOW, null)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
result.confirm()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
webview.webViewClient = if (preferencesHelper.eh_autoSolveCaptchas().getOrDefault()
|
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
// Fetch auto-solve credentials early for speed
|
|
credentialsObservable = httpClient.newCall(Request.Builder()
|
|
// Rob demo credentials
|
|
.url("https://speech-to-text-demo.ng.bluemix.net/api/v1/credentials")
|
|
.build())
|
|
.asObservableSuccess()
|
|
.subscribeOn(Schedulers.io())
|
|
.map {
|
|
val json = jsonParser.parse(it.body()!!.string())
|
|
it.close()
|
|
json["token"].string
|
|
}.melt()
|
|
|
|
webview.addJavascriptInterface(this@SolveCaptchaActivity, "exh")
|
|
AutoSolvingWebViewClient(this, source, script)
|
|
} else {
|
|
BasicWebViewClient(this, source, script)
|
|
}
|
|
|
|
webview.loadUrl(url)
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
cm.removeAllCookies { continueLoading() }
|
|
} else {
|
|
cm.removeAllCookie()
|
|
continueLoading()
|
|
}
|
|
|
|
setSupportActionBar(toolbar)
|
|
|
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
}
|
|
|
|
override fun onSupportNavigateUp(): Boolean {
|
|
finish()
|
|
return true
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
|
fun captchaSolveFail() {
|
|
currentLoopId = null
|
|
validateCurrentLoopId = null
|
|
Timber.e(IllegalStateException("Captcha solve failure!"))
|
|
runOnUiThread {
|
|
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
|
MaterialDialog.Builder(this)
|
|
.title("Captcha solve failure")
|
|
.content("Failed to auto-solve the captcha!")
|
|
.cancelable(true)
|
|
.canceledOnTouchOutside(true)
|
|
.positiveText("Ok")
|
|
.show()
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
@JavascriptInterface
|
|
fun callback(result: String?, loopId: String, stage: Int) {
|
|
if(loopId != currentLoopId) return
|
|
|
|
when(stage) {
|
|
STAGE_CHECKBOX -> {
|
|
if(result!!.toBoolean()) {
|
|
webview.postDelayed({
|
|
getAudioButtonLocation(loopId)
|
|
}, 250)
|
|
} else {
|
|
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 = webview.x + origX / iw * webview.width
|
|
val y = webview.y + origY / ih * webview.height
|
|
Timber.d("Found audio button coords: %f %f", x, y)
|
|
simulateClick(x + 50, y + 50)
|
|
webview.post {
|
|
doStageDownloadAudio(loopId)
|
|
}
|
|
} else {
|
|
webview.postDelayed({
|
|
getAudioButtonLocation(loopId)
|
|
}, 250)
|
|
}
|
|
}
|
|
STAGE_DOWNLOAD_AUDIO -> {
|
|
if(result != null) {
|
|
Timber.d("Got audio URL: $result")
|
|
performRecognize(result)
|
|
.observeOn(Schedulers.io())
|
|
.subscribe ({
|
|
Timber.d("Got audio transcript: $it")
|
|
webview.post {
|
|
typeResult(loopId, it!!
|
|
.replace(TRANSCRIPT_CLEANER_REGEX, "")
|
|
.replace(SPACE_DEDUPE_REGEX, " ")
|
|
.trim())
|
|
}
|
|
}, {
|
|
captchaSolveFail()
|
|
})
|
|
} else {
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun performRecognize(url: String): Single<String> {
|
|
return credentialsObservable.flatMap { token ->
|
|
httpClient.newCall(Request.Builder()
|
|
.url(url)
|
|
.build()).asObservableSuccess().map {
|
|
token to it
|
|
}
|
|
}.flatMap { (token, response) ->
|
|
val audioFile = response.body()!!.bytes()
|
|
|
|
httpClient.newCall(Request.Builder()
|
|
.url(HttpUrl.parse("https://stream.watsonplatform.net/speech-to-text/api/v1/recognize")!!
|
|
.newBuilder()
|
|
.addQueryParameter("watson-token", token)
|
|
.build())
|
|
.post(MultipartBody.Builder()
|
|
.setType(MultipartBody.FORM)
|
|
.addFormDataPart("jsonDescription", RECOGNIZE_JSON)
|
|
.addFormDataPart("audio.mp3",
|
|
"audio.mp3",
|
|
RequestBody.create(MediaType.parse("audio/mp3"), audioFile))
|
|
.build())
|
|
.build()).asObservableSuccess()
|
|
}.map { response ->
|
|
jsonParser.parse(response.body()!!.string())["results"][0]["alternatives"][0]["transcript"].string.trim()
|
|
}.toSingle()
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
fun doStageCheckbox(loopId: String) {
|
|
if(loopId != currentLoopId) return
|
|
|
|
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)
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
fun getAudioButtonLocation(loopId: String) {
|
|
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)
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
fun doStageDownloadAudio(loopId: String) {
|
|
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)
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
fun typeResult(loopId: String, result: String) {
|
|
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)
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
fun beginSolveLoop() {
|
|
val loopId = UUID.randomUUID().toString()
|
|
currentLoopId = loopId
|
|
doStageCheckbox(loopId)
|
|
}
|
|
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
@JavascriptInterface
|
|
fun validateCaptchaCallback(result: Boolean, loopId: String) {
|
|
if(loopId != validateCurrentLoopId) return
|
|
|
|
if(result) {
|
|
Timber.d("Captcha solved!")
|
|
webview.post {
|
|
webview.evaluateJavascript(SOLVE_UI_SCRIPT_HIDE, null)
|
|
}
|
|
val asbtn = intent.getStringExtra(ASBTN_EXTRA)
|
|
if(asbtn != null) {
|
|
webview.post {
|
|
webview.evaluateJavascript("(function() {document.querySelector('$asbtn').click();})();", null)
|
|
}
|
|
}
|
|
} else {
|
|
val savedStrictValidationStartTime = strictValidationStartTime
|
|
if(savedStrictValidationStartTime != null
|
|
&& System.currentTimeMillis() > savedStrictValidationStartTime) {
|
|
captchaSolveFail()
|
|
} else {
|
|
webview.postDelayed({
|
|
runValidateCaptcha(loopId)
|
|
}, 250)
|
|
}
|
|
}
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
fun runValidateCaptcha(loopId: String) {
|
|
if(loopId != validateCurrentLoopId) return
|
|
|
|
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)
|
|
}
|
|
|
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
|
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 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 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(" +")
|
|
|
|
fun launch(context: Context,
|
|
source: CaptchaCompletionVerifier,
|
|
cookies: Map<String, String>,
|
|
script: String,
|
|
url: String,
|
|
autoSolveSubmitBtnSelector: String? = null) {
|
|
val intent = Intent(context, SolveCaptchaActivity::class.java).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)
|
|
}
|
|
}
|
|
}
|
|
|
|
interface CaptchaCompletionVerifier : Source {
|
|
fun verifyNoCaptcha(url: String): Boolean
|
|
}
|
|
|