feat: cof experiments

This commit is contained in:
rhunk
2024-05-30 23:59:55 +02:00
parent 5d5a067319
commit d28733275b
6 changed files with 85 additions and 47 deletions

View File

@@ -973,10 +973,6 @@
"name": "Convert Message Locally",
"description": "Converts snaps to chat external media locally. This appears in chat download context menu"
},
"new_chat_action_menu": {
"name": "New Chat Action Menu",
"description": "Use the new chat action menu drawer"
},
"media_file_picker": {
"name": "Media File Picker",
"description": "Allows you to pick any video/audio file from the gallery"
@@ -999,6 +995,10 @@
}
}
},
"cof_experiments": {
"name": "COF Experiments",
"description": "Enables unreleased/beta Snapchat features"
},
"edit_message": {
"name": "Edit Messages",
"description": "Allows you to edit messages in conversations"

View File

@@ -8,6 +8,15 @@ import me.rhunk.snapenhance.common.config.ConfigFlag
import me.rhunk.snapenhance.common.config.FeatureNotice
class Experimental : ConfigContainer() {
companion object {
val cofExperimentList = listOf(
"android_action_menu_v2",
"android_action_menu_adjust_message_position",
"chat_emoji_reactions_sending_enabled",
"chat_text_message_plugin",
)
}
class ComposerHooksConfig: ConfigContainer(hasGlobalState = true) {
val showFirstCreatedUsername = boolean("show_first_created_username")
val bypassCameraRollLimit = boolean("bypass_camera_roll_limit")
@@ -42,12 +51,12 @@ class Experimental : ConfigContainer() {
val nativeHooks = container("native_hooks", NativeHooks()) { icon = Icons.Default.Memory; requireRestart() }
val spoof = container("spoof", Spoof()) { icon = Icons.Default.Fingerprint ; addNotices(FeatureNotice.BAN_RISK); requireRestart() }
val convertMessageLocally = boolean("convert_message_locally") { requireRestart() }
val newChatActionMenu = boolean("new_chat_action_menu") { requireRestart() }
val mediaFilePicker = boolean("media_file_picker") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }
val callRecorder = boolean("call_recorder") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }
val accountSwitcher = container("account_switcher", AccountSwitcherConfig()) { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val editMessage = boolean("edit_message") { requireRestart(); addNotices(FeatureNotice.BAN_RISK) }
val editMessage = boolean("edit_message") { requireRestart() }
val cofExperiments = multiple("cof_experiments", *cofExperimentList.toTypedArray()) { requireRestart(); addFlags(ConfigFlag.NO_TRANSLATE); addNotices(FeatureNotice.UNSTABLE) }
val appLock = container("app_lock", AppLockConfig()) { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val infiniteStoryBoost = boolean("infinite_story_boost")
val meoPasscodeBypass = boolean("meo_passcode_bypass")

View File

@@ -1,49 +1,40 @@
package me.rhunk.snapenhance.core.features.impl
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.OpenParams
import me.rhunk.snapenhance.common.util.ktx.getBlobOrNull
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.dataBuilder
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.mapper.impl.COFObservableMapper
import java.lang.reflect.Method
class COFOverride : Feature("Cof Override", loadParams = FeatureLoadParams.INIT_ASYNC) {
override fun asyncInit() {
val coreDatabaseFile = context.androidContext.getDatabasePath("core.db")
if (!coreDatabaseFile.exists()) return
SQLiteDatabase.openDatabase(coreDatabaseFile, OpenParams.Builder().apply {
setOpenFlags(SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
}.build()).use { db ->
fun setProperty(configId: String, value: Any) {
runCatching {
db.rawQuery("SELECT config_result FROM ConfigRule WHERE config_id = ?", arrayOf(configId)).use { cursor ->
if (!cursor.moveToFirst()) {
context.log.warn("Failed to find $configId in ConfigRule")
return
}
val configResult = cursor.getBlobOrNull("config_result")?.let {
ProtoEditor(it).apply {
edit(2) {
clear()
when (value) {
is Int -> addVarInt(1, value)
is Long -> addVarInt(2, value)
is Float -> addFixed32(3, value)
is Boolean -> addVarInt(4, if (value) 1 else 0)
is String -> addString(5, value)
is ByteArray -> addBuffer(6, value)
is Double -> addFixed64(7, value.toLong())
else -> return@edit
}
}
}.toByteArray()
} ?: return
db.execSQL("UPDATE ConfigRule SET config_result = ? WHERE config_id = ?", arrayOf(configResult, configId))
}
class COFOverride : Feature("COF Override", loadParams = FeatureLoadParams.INIT_SYNC) {
var hasActionMenuV2 = false
override fun init() {
val cofExperiments by context.config.experimental.cofExperiments
context.mappings.useMapper(COFObservableMapper::class) {
classReference.getAsClass()?.hook(getBooleanObservable.get() ?: return@useMapper, HookStage.AFTER) { param ->
val configId = param.arg<String>(0)
val result by lazy { param.getResult()?.getObjectField("b") }
fun setBooleanResult(state: Boolean) {
param.setResult((param.method() as Method).returnType.dataBuilder {
set("a", 4)
set("b", state)
})
}
if (cofExperiments.contains(configId.lowercase())) {
setBooleanResult(true)
}
if (configId == "ANDROID_ACTION_MENU_V2" && result == true) {
hasActionMenuV2 = true
}
}
setProperty("ANDROID_ACTION_MENU_V2", context.config.experimental.newChatActionMenu.get())
}
}
}

View File

@@ -10,6 +10,7 @@ import android.widget.ScrollView
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.COFOverride
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.ui.findParent
import me.rhunk.snapenhance.core.ui.menu.impl.*
@@ -52,6 +53,8 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id")
val chatActionMenu = context.resources.getIdentifier("chat_action_menu", "id")
val hasV2ActionMenu = { context.feature(COFOverride::class).hasActionMenuV2 }
context.event.subscribe(AddViewEvent::class) { event ->
val originalAddView: (View) -> Unit = {
event.adapter.invokeOriginal(arrayOf(it, -1,
@@ -99,12 +102,12 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
}
}
if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && context.config.experimental.newChatActionMenu.get()) {
if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && hasV2ActionMenu()) {
(menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event)
return@subscribe
}
if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !context.config.experimental.newChatActionMenu.get()) {
if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !hasV2ActionMenu()) {
if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe
menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView)
return@subscribe

View File

@@ -36,6 +36,7 @@ class ClassMapper(
OperaViewerParamsMapper(),
MemoriesPresenterMapper(),
StreaksExpirationMapper(),
COFObservableMapper(),
)
}

View File

@@ -0,0 +1,34 @@
package me.rhunk.snapenhance.mapper.impl
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.getClassName
class COFObservableMapper: AbstractClassMapper("COFObservable") {
val classReference = classReference("class")
val getBooleanObservable = string("getBooleanObservable")
init {
mapper {
for (classDef in classes) {
if (classDef.interfaces.isEmpty()) continue
if (classDef.methods.none { it.name == "dispose" }) continue
val getBooleanObservableDexMethod = classDef.methods.firstOrNull { method ->
method.parameterTypes.size == 2 &&
method.parameterTypes[0] == "Ljava/lang/String;" &&
getClass(method.returnType)?.methods?.any { it.name == "mergeFrom" } == true
} ?: continue
if (getBooleanObservableDexMethod.implementation?.instructions?.any { instruction ->
instruction is Instruction35c && (instruction.reference as? MethodReference)?.name == "elapsedRealtime"
} == true) {
getBooleanObservable.set(getBooleanObservableDexMethod.name)
classReference.set(classDef.getClassName())
return@mapper
}
}
}
}
}