Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
a85f399b29
3 changed files with 85 additions and 86 deletions
|
|
@ -52,4 +52,5 @@ dependencies {
|
|||
implementation("io.ktor:ktor-client-core:3.3.1")
|
||||
implementation("io.ktor:ktor-client-cio:3.3.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("dev.rushii:libunbound:1.0.0")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,6 @@ class Utils {
|
|||
|
||||
reloadApp()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
UpdaterModule.resetLoaderConfig(context)
|
||||
reloadApp()
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import android.app.AlertDialog
|
|||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import android.widget.Toast
|
||||
import androidx.core.util.writeBytes
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import dev.rushii.libunbound.LibUnbound
|
||||
import ShiggyXposed.xposed.Constants
|
||||
import ShiggyXposed.xposed.Module
|
||||
import ShiggyXposed.xposed.Utils
|
||||
|
|
@ -22,24 +22,20 @@ import io.ktor.client.statement.*
|
|||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@Serializable
|
||||
data class CustomLoadUrl(
|
||||
val enabled: Boolean = false, val url: String = ""
|
||||
)
|
||||
data class CustomLoadUrl(val enabled: Boolean = false, val url: String = "")
|
||||
|
||||
@Serializable
|
||||
data class LoaderConfig(
|
||||
val customLoadUrl: CustomLoadUrl = CustomLoadUrl()
|
||||
)
|
||||
data class LoaderConfig(val customLoadUrl: CustomLoadUrl = CustomLoadUrl(), val disableInjection: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
data class EndpointInfo(val paths: ArrayList<String>, val hash: String? = null, val version: String)
|
||||
|
||||
/**
|
||||
* Module that updates the JS bundle by downloading it from a remote URL.
|
||||
*
|
||||
* Shows dialogs when failed allowing retry.
|
||||
*/
|
||||
object UpdaterModule : Module() {
|
||||
private lateinit var config: LoaderConfig
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
|
@ -48,20 +44,16 @@ object UpdaterModule : Module() {
|
|||
private lateinit var cacheDir: File
|
||||
private lateinit var bundle: File
|
||||
private lateinit var etag: File
|
||||
// Path to the application's data directory so we can read LogBox settings
|
||||
private lateinit var appDataDir: File
|
||||
|
||||
private const val TIMEOUT_CACHED = 5000L
|
||||
private const val TIMEOUT = 10000L
|
||||
private const val TIMEOUT_STRICT = 15000L
|
||||
private const val MIN_BYTECODE_SIZE = 512
|
||||
private const val ETAG_FILE = "etag.txt"
|
||||
private const val CONFIG_FILE = "loader.json"
|
||||
|
||||
private const val DEFAULT_BUNDLE_URL =
|
||||
"https://github.com/kmmiio99o/ShiggyCord/releases/latest/download/shiggycord.js"
|
||||
private const val DEFAULT_BASE_URL = "https://github.com/kmmiio99o/ShiggyCord/releases/latest/download/"
|
||||
private const val DEFAULT_BUNDLE_NAME = "shiggycord.min.js"
|
||||
|
||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||
// store app data dir for later checks (LogBox settings live under files/logbox)
|
||||
appDataDir = File(appInfo.dataDir)
|
||||
cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() }
|
||||
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
||||
|
||||
|
|
@ -69,91 +61,102 @@ object UpdaterModule : Module() {
|
|||
etag = File(cacheDir, ETAG_FILE)
|
||||
|
||||
val configFile = File(filesDir, CONFIG_FILE)
|
||||
|
||||
config = runCatching {
|
||||
if (configFile.exists()) {
|
||||
JSON.decodeFromString<LoaderConfig>(configFile.readText())
|
||||
} else LoaderConfig()
|
||||
if (configFile.exists()) JSON.decodeFromString<LoaderConfig>(configFile.readText()) else LoaderConfig()
|
||||
}.getOrDefault(LoaderConfig())
|
||||
}
|
||||
|
||||
fun downloadScript(activity: Activity? = null): Job = scope.launch {
|
||||
try {
|
||||
// Respect LogBox setting to disable bundle injections.
|
||||
// If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true,
|
||||
// skip attempting to download/update the bundle.
|
||||
try {
|
||||
val settingsFile = File(appDataDir, "files/logbox/LOGBOX_SETTINGS")
|
||||
if (settingsFile.exists()) {
|
||||
val txt = settingsFile.readText()
|
||||
// look for a simple true marker; this avoids introducing a new JSON dependency here
|
||||
if (txt.contains("\"bundleInjectionDisabled\"") && txt.contains("true")) {
|
||||
Log.i("UpdaterModule: bundle injection disabled by LogBox settings — skipping download")
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
} catch (ignore: Exception) {
|
||||
// If anything goes wrong reading the setting, proceed with normal behaviour.
|
||||
}
|
||||
|
||||
HttpClient(CIO) {
|
||||
expectSuccess = false
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = TIMEOUT_STRICT
|
||||
connectTimeoutMillis = 5000L
|
||||
socketTimeoutMillis = TIMEOUT_STRICT
|
||||
}
|
||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
||||
install(HttpRedirect) {}
|
||||
install(HttpRedirect) { checkHttpMethod = false }
|
||||
}.use { client ->
|
||||
val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL
|
||||
Log.i("Fetching JS bundle from: $url")
|
||||
val targetUrl = resolveTargetUrl(client)
|
||||
Log.i("Fetching bundle: $targetUrl")
|
||||
|
||||
val response: HttpResponse = client.get(url) {
|
||||
val response: HttpResponse = client.get(targetUrl) {
|
||||
headers {
|
||||
if (etag.exists() && bundle.exists()) {
|
||||
append(HttpHeaders.IfNoneMatch, etag.readText())
|
||||
}
|
||||
}
|
||||
|
||||
// Retries don't need timeout
|
||||
if (activity == null) {
|
||||
timeout {
|
||||
requestTimeoutMillis = if (!bundle.exists()) TIMEOUT else TIMEOUT_CACHED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val bytes: ByteArray = response.body()
|
||||
AtomicFile(bundle).writeBytes(bytes)
|
||||
|
||||
val newTag = response.headers[HttpHeaders.ETag]
|
||||
if (!newTag.isNullOrEmpty()) etag.writeText(newTag) else etag.delete()
|
||||
if (bytes.size < MIN_BYTECODE_SIZE) {
|
||||
throw Exception("Payload too small (${bytes.size} bytes). Possible corrupt build.")
|
||||
}
|
||||
|
||||
Log.i("Bundle updated (${bytes.size} bytes)")
|
||||
AtomicFile(bundle).apply {
|
||||
val stream = startWrite()
|
||||
try {
|
||||
stream.write(bytes)
|
||||
finishWrite(stream)
|
||||
} catch (e: Exception) {
|
||||
failWrite(stream)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
response.headers[HttpHeaders.ETag]?.let { etag.writeText(it) } ?: etag.delete()
|
||||
Log.i("Bundle updated: ${bytes.size} bytes")
|
||||
|
||||
// This is a retry, so we show a dialog
|
||||
if (activity != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
AlertDialog.Builder(activity).setTitle("ShiggyCord Update Successful")
|
||||
.setMessage("A reload is required for changes to take effect.")
|
||||
.setPositiveButton("Reload") { dialog, _ ->
|
||||
reloadApp()
|
||||
dialog.dismiss()
|
||||
}.setCancelable(false).show()
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle("Update Successful")
|
||||
.setMessage("Reload required.")
|
||||
.setPositiveButton("Reload") { _, _ -> reloadApp() }
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpStatusCode.NotModified -> {
|
||||
Log.i("Server responded with 304, no changes")
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw ResponseException(response, "Received status: ${response.status}")
|
||||
}
|
||||
HttpStatusCode.NotModified -> Log.i("Bundle is up to date (304)")
|
||||
else -> throw ResponseException(response, "HTTP ${response.status}")
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e("Failed to download script", e)
|
||||
showErrorDialog(e)
|
||||
Log.e("Updater Error", e)
|
||||
if (activity != null) withContext(Dispatchers.Main) {
|
||||
Toast.makeText(activity, "Update failed: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveTargetUrl(client: HttpClient): String {
|
||||
if (config.customLoadUrl.enabled && config.customLoadUrl.url.isNotEmpty()) {
|
||||
return config.customLoadUrl.url
|
||||
}
|
||||
|
||||
return try {
|
||||
val infoResponse = client.get("${DEFAULT_BASE_URL}info.json")
|
||||
if (infoResponse.status == HttpStatusCode.OK) {
|
||||
val info = JSON.decodeFromString<EndpointInfo>(infoResponse.bodyAsText())
|
||||
val hermesVersion = withTimeoutOrNull(2000L) {
|
||||
runCatching { LibUnbound.getHermesRuntimeBytecodeVersion() }.getOrNull()
|
||||
} ?: 96
|
||||
|
||||
val hbcName = "kettu.$hermesVersion.hbc"
|
||||
when {
|
||||
info.paths.contains(hbcName) -> DEFAULT_BASE_URL + hbcName
|
||||
info.paths.contains("kettu.min.js") -> DEFAULT_BASE_URL + "kettu.min.js"
|
||||
else -> DEFAULT_BASE_URL + DEFAULT_BUNDLE_NAME
|
||||
}
|
||||
} else DEFAULT_BASE_URL + DEFAULT_BUNDLE_NAME
|
||||
} catch (e: Exception) {
|
||||
DEFAULT_BASE_URL + DEFAULT_BUNDLE_NAME
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,16 +164,16 @@ object UpdaterModule : Module() {
|
|||
lastActivity = WeakReference(activity)
|
||||
}
|
||||
|
||||
fun showErrorDialog(e: Throwable) {
|
||||
val activity = lastActivity?.get() ?: return
|
||||
|
||||
activity.runOnUiThread {
|
||||
}
|
||||
fun setDisableInjection(context: Context, disabled: Boolean) {
|
||||
val filesDir = File(context.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
||||
val configFile = File(filesDir, CONFIG_FILE)
|
||||
val newCfg = config.copy(disableInjection = disabled)
|
||||
configFile.writeText(JSON.encodeToString(newCfg))
|
||||
config = newCfg
|
||||
Toast.makeText(context, "Injection ${if (disabled) "disabled" else "enabled"}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
fun resetLoaderConfig(context: Context) {
|
||||
val filesDir = File(context.dataDir, Constants.FILES_DIR)
|
||||
val config = File(filesDir, CONFIG_FILE)
|
||||
if (config.exists()) config.delete()
|
||||
fun isInjectionDisabled(context: Context? = null): Boolean {
|
||||
return if (::config.isInitialized) config.disableInjection else false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue