unyeet (#14906)
* unyeet * make regex lazy * what was that * remove from autocloser and removed_sources.md * add filters * suggestions * chore: bump version code * more selectFirsts
This commit is contained in:
parent
5dc59917af
commit
95e6d6504b
|
@ -37,7 +37,7 @@ jobs:
|
|||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": ".*(mangago|mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|cocomanga|hitomi\\.la|copymanga|neox|1manga\\.co|mangafox\\.fun|mangahere\\.onl|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangatoday|manga\\.town|onemanga\\.info|koushoku).*",
|
||||
"regex": ".*(mangafox|hq\\s*dragon|manga\\s*host|supermangas|superhentais|union\\s*mangas|yes\\s*mangas|manhuascan|manhwahot|leitor\\.?net|manga\\s*livre|tsuki\\s*mangas|manga\\s*yabu|mangas\\.in|mangas\\.pw|hentaikai|toptoon\\+?|cocomanga|hitomi\\.la|copymanga|neox|1manga\\.co|mangafox\\.fun|mangahere\\.onl|mangakakalot\\.fun|manganel(?!o)|mangaonline\\.fun|mangatoday|manga\\.town|onemanga\\.info|koushoku).*",
|
||||
"ignoreCase": true,
|
||||
"message": "{match} will not be added back as it is too difficult to maintain. Read #3475 for more information."
|
||||
},
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
- Koushoku https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13329
|
||||
- Mangá Host https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7065
|
||||
- Mangá Livre and Leitor.net https://github.com/tachiyomiorg/tachiyomi-extensions/pull/8679
|
||||
- mangago.me https://github.com/tachiyomiorg/tachiyomi-extensions/issues/988
|
||||
- MangaYabu! https://github.com/tachiyomiorg/tachiyomi-extensions/pull/9336
|
||||
- ManhuaScan https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7129
|
||||
- ManhwaHot https://github.com/tachiyomiorg/tachiyomi-extensions/pull/7129
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,16 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Mangago'
|
||||
pkgNameSuffix = 'en.mangago'
|
||||
extClass = '.Mangago'
|
||||
extVersionCode = 8
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib-cryptoaes'))
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
|
@ -0,0 +1,459 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangago
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.Base64
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URLEncoder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class Mangago : ParsedHttpSource() {
|
||||
|
||||
override val name = "Mangago"
|
||||
|
||||
override val baseUrl = "https://www.mangago.me"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder().addInterceptor { chain ->
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
val key = response.request.url.queryParameter("desckey") ?: return@addInterceptor response
|
||||
val cols = response.request.url.queryParameter("cols")?.toIntOrNull() ?: return@addInterceptor response
|
||||
|
||||
val image = unscrambleImage(response.body!!.byteStream(), key, cols)
|
||||
val body = image.toResponseBody("image/jpeg".toMediaTypeOrNull())
|
||||
return@addInterceptor response.newBuilder()
|
||||
.body(body)
|
||||
.build()
|
||||
}.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("Cookie", cookiesHeader)
|
||||
|
||||
private val cookiesHeader by lazy {
|
||||
val cookies = mutableMapOf<String, String>()
|
||||
|
||||
// Needed for correct page ordering
|
||||
cookies["_m_superu"] = "1"
|
||||
|
||||
buildCookies(cookies)
|
||||
}
|
||||
|
||||
private val genreListingSelector = ".updatesli"
|
||||
|
||||
private val genreListingNextPageSelector = ".current+li > a"
|
||||
|
||||
private val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||
|
||||
private fun mangaFromElement(element: Element) = SManga.create().apply {
|
||||
val linkElement = element.selectFirst(".thm-effect")
|
||||
|
||||
setUrlWithoutDomain(linkElement.attr("href"))
|
||||
title = linkElement.attr("title")
|
||||
|
||||
val thumbnailElem = linkElement.selectFirst("img")
|
||||
thumbnail_url = thumbnailElem.attr("abs:src").ifBlank { thumbnailElem.attr("abs:data-src") }
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/genre/all/$page/?f=1&o=1&sortby=view&e=", headers)
|
||||
|
||||
override fun popularMangaSelector(): String = genreListingSelector
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = genreListingNextPageSelector
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/genre/all/$page/?f=1&o=1&sortby=update_date&e=", headers)
|
||||
|
||||
override fun latestUpdatesSelector() = genreListingSelector
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = genreListingNextPageSelector
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
"$baseUrl/r/l_search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("name", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build().toString()
|
||||
} else {
|
||||
"$baseUrl/genre/".toHttpUrl().newBuilder().apply {
|
||||
val genres = mutableListOf<String>()
|
||||
val genresEx = mutableListOf<String>()
|
||||
|
||||
filters.ifEmpty { getFilterList() }.forEach {
|
||||
when (it) {
|
||||
is UriFilter -> it.addToUrl(this)
|
||||
is GenreFilterGroup -> it.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_EXCLUDE -> genresEx.add(genre.name)
|
||||
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (genres.isEmpty()) {
|
||||
addPathSegment("all")
|
||||
} else {
|
||||
addPathSegment(genres.joinToString(","))
|
||||
}
|
||||
addPathSegment(page.toString())
|
||||
|
||||
addQueryParameter("e", genresEx.joinToString(","))
|
||||
}.build().toString()
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "$genreListingSelector, .pic_list .box"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = genreListingNextPageSelector
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val coverElement = document.select(".left.cover > img")
|
||||
|
||||
title = coverElement.attr("alt")
|
||||
thumbnail_url = coverElement.attr("src")
|
||||
document.select(".manga_right td").forEach {
|
||||
when (it.getElementsByTag("label").text().trim().lowercase()) {
|
||||
"status:" -> {
|
||||
status = when (it.selectFirst("span").text().trim().lowercase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
"author:" -> {
|
||||
author = it.selectFirst("a").text()
|
||||
}
|
||||
"genre(s):" -> {
|
||||
genre = it.getElementsByTag("a").joinToString { it.text() }
|
||||
}
|
||||
}
|
||||
}
|
||||
description = document.selectFirst(".manga_summary").ownText().trim()
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "#chapter_table > tbody > tr"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
val link = element.getElementsByTag("a")
|
||||
|
||||
setUrlWithoutDomain(link.attr("href"))
|
||||
name = link.text().trim()
|
||||
date_upload = kotlin.runCatching {
|
||||
dateFormat.parse(element.getElementsByClass("no").text().trim())?.time
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val imgsrcsScript = document.selectFirst("script:containsData(imgsrcs)")?.html()
|
||||
?: throw Exception("Could not find imgsrcs")
|
||||
val imgsrcRaw = imgSrcsRegex.find(imgsrcsScript)?.groupValues?.get(1)
|
||||
?: throw Exception("Could not extract imgsrcs")
|
||||
val imgsrcs = Base64.decode(imgsrcRaw, Base64.DEFAULT)
|
||||
|
||||
val chapterJsUrl = document.getElementsByTag("script").first {
|
||||
it.attr("src").contains("chapter.js", ignoreCase = true)
|
||||
}.attr("abs:src")
|
||||
|
||||
val obfuscatedChapterJs = client.newCall(GET(chapterJsUrl, headers)).execute().body!!.string()
|
||||
val deobfChapterJs = SoJsonV4Deobfuscator.decode(obfuscatedChapterJs)
|
||||
|
||||
val key = findHexEncodedVariable(deobfChapterJs, "key").decodeHex()
|
||||
val iv = findHexEncodedVariable(deobfChapterJs, "iv").decodeHex()
|
||||
val cipher = Cipher.getInstance(hashCipher)
|
||||
val keyS = SecretKeySpec(key, aes)
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(iv))
|
||||
|
||||
var imageList = cipher.doFinal(imgsrcs).toString(Charsets.UTF_8)
|
||||
|
||||
try {
|
||||
val keyLocations = keyLocationRegex.findAll(deobfChapterJs).map {
|
||||
it.groupValues[1].toInt()
|
||||
}.distinct()
|
||||
|
||||
val unscrambleKey = keyLocations.map {
|
||||
imageList[it].toString().toInt()
|
||||
}.toList()
|
||||
|
||||
keyLocations.forEachIndexed { idx, it ->
|
||||
imageList = imageList.removeRange(it - idx..it - idx)
|
||||
}
|
||||
|
||||
imageList = imageList.unscramble(unscrambleKey)
|
||||
} catch (e: NumberFormatException) {
|
||||
// Only call where it should throw is imageList[it].toString().toInt().
|
||||
// This usually means that the list is already unscrambled.
|
||||
}
|
||||
|
||||
val cols = deobfChapterJs
|
||||
.substringAfter("var widthnum=heightnum=")
|
||||
.substringBefore(";")
|
||||
|
||||
return imageList
|
||||
.split(",")
|
||||
.mapIndexed { idx, it ->
|
||||
val url = if (it.contains("cspiclink")) {
|
||||
"$it?desckey=${getDescramblingKey(deobfChapterJs, it)}&cols=$cols"
|
||||
} else {
|
||||
it
|
||||
}
|
||||
|
||||
Page(idx, imageUrl = url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("Ignored if using text search"),
|
||||
StatusFilterGroup(),
|
||||
SortFilter(),
|
||||
GenreFilterGroup(),
|
||||
)
|
||||
|
||||
private interface UriFilter {
|
||||
fun addToUrl(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
private class StatusFilter(name: String, val query: String) : UriFilter, Filter.CheckBox(name) {
|
||||
override fun addToUrl(builder: HttpUrl.Builder) {
|
||||
builder.addQueryParameter(query, if (state) "1" else "0")
|
||||
}
|
||||
}
|
||||
|
||||
private class StatusFilterGroup : UriFilter, Filter.Group<StatusFilter>(
|
||||
"Status",
|
||||
listOf(
|
||||
StatusFilter("Completed", "f"),
|
||||
StatusFilter("Ongoing", "o")
|
||||
)
|
||||
) {
|
||||
override fun addToUrl(builder: HttpUrl.Builder) {
|
||||
state.forEach {
|
||||
it.addToUrl(builder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class UriPartFilter(
|
||||
name: String,
|
||||
private val query: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
private val firstIsUnspecified: Boolean = true,
|
||||
state: Int = 0
|
||||
) : UriFilter, Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
||||
override fun addToUrl(builder: HttpUrl.Builder) {
|
||||
if (state != 0 || !firstIsUnspecified) {
|
||||
builder.addQueryParameter(query, vals[state].second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SortFilter : UriPartFilter(
|
||||
"Sort",
|
||||
"sortby",
|
||||
arrayOf(
|
||||
Pair("Random", "random"),
|
||||
Pair("Views", "view"),
|
||||
Pair("Comment Count", "comment_count"),
|
||||
Pair("Creation Date", "create_date"),
|
||||
Pair("Update Date", "update_date")
|
||||
),
|
||||
state = 1,
|
||||
)
|
||||
|
||||
private class GenreFilter(name: String) : Filter.TriState(name)
|
||||
|
||||
private class GenreFilterGroup : Filter.Group<GenreFilter>(
|
||||
"Genres",
|
||||
listOf(
|
||||
GenreFilter("Yaoi"),
|
||||
GenreFilter("Doujinshi"),
|
||||
GenreFilter("Shounen Ai"),
|
||||
GenreFilter("Shoujo"),
|
||||
GenreFilter("Yuri"),
|
||||
GenreFilter("Romance"),
|
||||
GenreFilter("Fantasy"),
|
||||
GenreFilter("Comedy"),
|
||||
GenreFilter("Smut"),
|
||||
GenreFilter("Adult"),
|
||||
GenreFilter("School Life"),
|
||||
GenreFilter("Mystery"),
|
||||
GenreFilter("One Shot"),
|
||||
GenreFilter("Ecchi"),
|
||||
GenreFilter("Shounen"),
|
||||
GenreFilter("Martial Arts"),
|
||||
GenreFilter("Shoujo Ai"),
|
||||
GenreFilter("Supernatural"),
|
||||
GenreFilter("Drama"),
|
||||
GenreFilter("Action"),
|
||||
GenreFilter("Adventure"),
|
||||
GenreFilter("Harem"),
|
||||
GenreFilter("Historical"),
|
||||
GenreFilter("Horror"),
|
||||
GenreFilter("Josei"),
|
||||
GenreFilter("Mature"),
|
||||
GenreFilter("Mecha"),
|
||||
GenreFilter("Psychological"),
|
||||
GenreFilter("Sci-fi"),
|
||||
GenreFilter("Seinen"),
|
||||
GenreFilter("Slice Of Life"),
|
||||
GenreFilter("Sports"),
|
||||
GenreFilter("Gender Bender"),
|
||||
GenreFilter("Tragedy"),
|
||||
GenreFilter("Bara"),
|
||||
GenreFilter("Shotacon"),
|
||||
GenreFilter("Webtoons")
|
||||
)
|
||||
)
|
||||
|
||||
private fun findHexEncodedVariable(input: String, variable: String): String {
|
||||
val regex = Regex("""var $variable\s*=\s*CryptoJS\.enc\.Hex\.parse\("([0-9a-zA-Z]+)"\)""")
|
||||
return regex.find(input)?.groupValues?.get(1) ?: ""
|
||||
}
|
||||
|
||||
private fun String.unscramble(keys: List<Int>): String {
|
||||
var s = this
|
||||
keys.reversed().forEach {
|
||||
for (i in s.length - 1 downTo it) {
|
||||
if (i % 2 != 0) {
|
||||
val temp = s[i - it]
|
||||
s = s.replaceRange(i - it..i - it, s[i].toString())
|
||||
s = s.replaceRange(i..i, temp.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
private fun unscrambleImage(image: InputStream, key: String, cols: Int): ByteArray {
|
||||
val bitmap = BitmapFactory.decodeStream(image)
|
||||
|
||||
val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
|
||||
val unitWidth = bitmap.width / cols
|
||||
val unitHeight = bitmap.height / cols
|
||||
|
||||
val keyArray = key.split("a")
|
||||
|
||||
for (idx in 0 until cols * cols) {
|
||||
val keyval = keyArray[idx].ifEmpty { "0" }.toInt()
|
||||
|
||||
val heightY = keyval.floorDiv(cols)
|
||||
val dy = heightY * unitHeight
|
||||
val dx = (keyval - heightY * cols) * unitWidth
|
||||
|
||||
val widthY = idx.floorDiv(cols)
|
||||
val sy = widthY * unitHeight
|
||||
val sx = (idx - widthY * cols) * unitWidth
|
||||
|
||||
val srcRect = Rect(sx, sy, sx + unitWidth, sy + unitHeight)
|
||||
val dstRect = Rect(dx, dy, dx + unitWidth, dy + unitHeight)
|
||||
|
||||
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
private fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||
}
|
||||
|
||||
private fun String.decodeHex(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
|
||||
return chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
private fun getDescramblingKey(deobfChapterJs: String, imageUrl: String): String {
|
||||
val imgkeys = deobfChapterJs
|
||||
.substringAfter("var renImg = function(img,width,height,id){")
|
||||
.substringBefore("key = key.split(")
|
||||
.split("\n")
|
||||
.filter { jsFilters.all { filter -> !it.contains(filter) } }
|
||||
.joinToString("\n")
|
||||
.replace("img.src", "url")
|
||||
|
||||
val js = """
|
||||
function getDescramblingKey(url) { $imgkeys; return key; }
|
||||
getDescramblingKey("$imageUrl");
|
||||
""".trimIndent()
|
||||
|
||||
return QuickJs.create().use {
|
||||
it.execute(replacePosBytecode)
|
||||
|
||||
it.evaluate(js).toString()
|
||||
}
|
||||
}
|
||||
|
||||
private val jsFilters = listOf("jQuery", "document", "getContext", "toDataURL", "getImageData", "width", "height")
|
||||
|
||||
private val hashCipher = "AES/CBC/ZEROBYTEPADDING"
|
||||
|
||||
private val aes = "AES"
|
||||
|
||||
private val keyLocationRegex by lazy {
|
||||
Regex("""str\.charAt\(\s*(\d+)\s*\)""")
|
||||
}
|
||||
|
||||
private val imgSrcsRegex by lazy {
|
||||
Regex("""var imgsrcs\s*=\s*['"]([a-zA-Z0-9+=/]+)['"]""")
|
||||
}
|
||||
|
||||
private val replacePosBytecode by lazy {
|
||||
QuickJs.create().use {
|
||||
it.compile(
|
||||
"""
|
||||
function replacePos(strObj, pos, replacetext) {
|
||||
var str = strObj.substr(0, pos) + replacetext + strObj.substring(pos + 1, strObj.length);
|
||||
return str;
|
||||
}
|
||||
""".trimIndent(),
|
||||
"?"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package eu.kanade.tachiyomi.extension.en.mangago
|
||||
|
||||
import kotlin.IllegalArgumentException
|
||||
|
||||
/*
|
||||
Ported from https://github.com/hax0r31337/JSDec/blob/master/js/dec.js
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
Copyright (c) 2020 liulihaocai
|
||||
*/
|
||||
object SoJsonV4Deobfuscator {
|
||||
private val splitRegex: Regex = Regex("""[a-zA-Z]+""")
|
||||
|
||||
fun decode(jsf: String): String {
|
||||
if (!jsf.startsWith("['sojson.v4']")) {
|
||||
throw IllegalArgumentException("Obfuscated code is not sojson.v4")
|
||||
}
|
||||
|
||||
val args = jsf.substring(240, jsf.length - 59).split(splitRegex)
|
||||
|
||||
return args.map { it.toInt().toChar() }.joinToString("")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue