refactor: mapper

This commit is contained in:
rhunk
2024-01-11 23:10:09 +01:00
parent 1f7f270766
commit ed4334c429
44 changed files with 767 additions and 644 deletions

View File

@ -195,7 +195,7 @@ class RemoteSideContext(
} }
} }
if (mappings.isMappingsOutdated() || !mappings.isMappingsLoaded()) { if (mappings.isMappingsOutdated() || !mappings.isMappingsLoaded) {
requirements = requirements or Requirements.MAPPINGS requirements = requirements or Requirements.MAPPINGS
} }

View File

@ -110,7 +110,7 @@ class HomeSection : Section() {
} }
override fun onResumed() { override fun onResumed() {
if (!context.mappings.isMappingsLoaded()) { if (!context.mappings.isMappingsLoaded) {
context.mappings.init(context.androidContext) context.mappings.init(context.androidContext)
} }
context.coroutineScope.launch { context.coroutineScope.launch {

View File

@ -1,44 +1,24 @@
package me.rhunk.snapenhance.common.bridge.wrapper package me.rhunk.snapenhance.common.bridge.wrapper
import android.content.Context import android.content.Context
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.common.BuildConfig import me.rhunk.snapenhance.common.BuildConfig
import me.rhunk.snapenhance.common.Constants import me.rhunk.snapenhance.common.Constants
import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
import me.rhunk.snapenhance.mapper.Mapper import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.mapper.impl.* import me.rhunk.snapenhance.mapper.AbstractClassMapper
import java.util.concurrent.ConcurrentHashMap import me.rhunk.snapenhance.mapper.ClassMapper
import kotlin.system.measureTimeMillis import kotlin.reflect.KClass
class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) { class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteArray(Charsets.UTF_8)) {
companion object {
private val gson = GsonBuilder().setPrettyPrinting().create()
private val mappers = arrayOf(
BCryptClassMapper::class,
CallbackMapper::class,
DefaultMediaItemMapper::class,
MediaQualityLevelProviderMapper::class,
OperaPageViewControllerMapper::class,
PlusSubscriptionMapper::class,
ScCameraSettingsMapper::class,
StoryBoostStateMapper::class,
FriendsFeedEventDispatcherMapper::class,
CompositeConfigurationProviderMapper::class,
ScoreUpdateMapper::class,
FriendRelationshipChangerMapper::class,
ViewBinderMapper::class,
FriendingDataSourcesMapper::class,
OperaViewerParamsMapper::class,
)
}
private lateinit var context: Context private lateinit var context: Context
private val mappings = ConcurrentHashMap<String, Any>()
private var mappingUniqueHash: Long = 0 private var mappingUniqueHash: Long = 0
var isMappingsLoaded = false
private set
private val mappers = ClassMapper.DEFAULT_MAPPERS.associateBy { it::class }
private fun getUniqueBuildId() = (getSnapchatPackageInfo()?.longVersionCode ?: -1) xor BuildConfig.BUILD_HASH.hashCode().toLong() private fun getUniqueBuildId() = (getSnapchatPackageInfo()?.longVersionCode ?: -1) xor BuildConfig.BUILD_HASH.hashCode().toLong()
@ -63,8 +43,7 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
}.getOrNull() }.getOrNull()
fun getGeneratedBuildNumber() = mappingUniqueHash fun getGeneratedBuildNumber() = mappingUniqueHash
fun isMappingsOutdated() = mappingUniqueHash != getUniqueBuildId() || isMappingsLoaded().not() fun isMappingsOutdated() = mappingUniqueHash != getUniqueBuildId() || isMappingsLoaded.not()
fun isMappingsLoaded() = mappings.isNotEmpty()
private fun loadCached() { private fun loadCached() {
if (!isFileExists()) { if (!isFileExists()) {
@ -74,64 +53,39 @@ class MappingsWrapper : FileLoaderWrapper(BridgeFileType.MAPPINGS, "{}".toByteAr
mappingUniqueHash = it["unique_hash"].asLong mappingUniqueHash = it["unique_hash"].asLong
} }
mappingsObject.entrySet().forEach { (key, value): Map.Entry<String, JsonElement> -> mappingsObject.entrySet().forEach { (key, value) ->
if (value.isJsonArray) { mappers.values.firstOrNull { it.mapperName == key }?.let { mapper ->
mappings[key] = gson.fromJson(value, ArrayList::class.java) mapper.readFromJson(value.asJsonObject)
return@forEach mapper.classLoader = context.classLoader
} }
if (value.isJsonObject) {
mappings[key] = gson.fromJson(value, ConcurrentHashMap::class.java)
return@forEach
}
mappings[key] = value.asString
} }
isMappingsLoaded = true
} }
fun refresh() { fun refresh() {
mappingUniqueHash = getUniqueBuildId() mappingUniqueHash = getUniqueBuildId()
val mapper = Mapper(*mappers) val classMapper = ClassMapper(*mappers.values.toTypedArray())
runCatching { runCatching {
mapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK")) classMapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK"))
}.onFailure { }.onFailure {
throw Exception("Failed to load APK", it) throw Exception("Failed to load APK", it)
} }
measureTimeMillis { runBlocking {
val result = mapper.start().apply { val result = classMapper.run().apply {
addProperty("unique_hash", mappingUniqueHash) addProperty("unique_hash", mappingUniqueHash)
} }
write(result.toString().toByteArray()) write(result.toString().toByteArray())
} }
} }
fun getMappedObject(key: String): Any {
if (mappings.containsKey(key)) {
return mappings[key]!!
}
throw Exception("No mapping found for $key")
}
fun getMappedObjectNullable(key: String): Any? {
return mappings[key]
}
fun getMappedClass(className: String): Class<*>? {
return runCatching {
context.classLoader.loadClass(getMappedObject(className) as? String)
}.getOrNull()
}
fun getMappedClass(key: String, subKey: String): Class<*> {
return context.classLoader.loadClass(getMappedValue(key, subKey))
}
fun getMappedValue(key: String, subKey: String): String? {
return getMappedMap(key)?.get(subKey) as? String
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun getMappedMap(key: String): Map<String, *>? { fun <T : AbstractClassMapper> useMapper(type: KClass<T>, callback: T.() -> Unit) {
return getMappedObjectNullable(key) as? Map<String, *> mappers[type]?.let {
callback(it as? T ?: return)
} ?: run {
AbstractLogger.directError("Mapper ${type.simpleName} is not registered", Throwable())
}
} }
} }

View File

@ -91,7 +91,7 @@ class SnapEnhance {
hookMainActivity("onCreate") { hookMainActivity("onCreate") {
val isMainActivityNotNull = appContext.mainActivity != null val isMainActivityNotNull = appContext.mainActivity != null
appContext.mainActivity = this appContext.mainActivity = this
if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded()) return@hookMainActivity if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded) return@hookMainActivity
onActivityCreate() onActivityCreate()
jetpackComposeResourceHook() jetpackComposeResourceHook()
appContext.actionManager.onNewIntent(intent) appContext.actionManager.onNewIntent(intent)
@ -146,7 +146,7 @@ class SnapEnhance {
database.init() database.init()
eventDispatcher.init() eventDispatcher.init()
//if mappings aren't loaded, we can't initialize features //if mappings aren't loaded, we can't initialize features
if (!mappings.isMappingsLoaded()) return if (!mappings.isMappingsLoaded) return
bridgeClient.registerMessagingBridge(messagingBridge) bridgeClient.registerMessagingBridge(messagingBridge)
features.init() features.init()
scriptRuntime.connect(bridgeClient.getScriptingInterface()) scriptRuntime.connect(bridgeClient.getScriptingInterface())

View File

@ -12,6 +12,7 @@ import me.rhunk.snapenhance.core.features.impl.experiments.AddFriendSourceSpoof
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.messaging.EnumBulkAction import me.rhunk.snapenhance.core.messaging.EnumBulkAction
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper
class BulkMessagingAction : AbstractAction() { class BulkMessagingAction : AbstractAction() {
private val translation by lazy { context.translation.getCategory("bulk_messaging_action") } private val translation by lazy { context.translation.getCategory("bulk_messaging_action") }
@ -145,22 +146,23 @@ class BulkMessagingAction : AbstractAction() {
} }
private fun removeFriend(userId: String) { private fun removeFriend(userId: String) {
val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get FriendRelationshipChanger mapping") context.mappings.useMapper(FriendRelationshipChangerMapper::class) {
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!! val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!!
val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first {
it.name == this.removeFriendMethod.get()
}
val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first { val completable = removeFriendMethod.invoke(friendRelationshipChangerInstance,
it.name == friendRelationshipChangerMapping["removeFriendMethod"].toString() userId, // userId
removeFriendMethod.parameterTypes[1].enumConstants.first { it.toString() == "DELETED_BY_MY_FRIENDS" }, // source
null, // unknown
null, // unknown
null // InteractionPlacementInfo
)!!
completable::class.java.methods.first {
it.name == "subscribe" && it.parameterTypes.isEmpty()
}.invoke(completable)
} }
val completable = removeFriendMethod.invoke(friendRelationshipChangerInstance,
userId, // userId
removeFriendMethod.parameterTypes[1].enumConstants.first { it.toString() == "DELETED_BY_MY_FRIENDS" }, // source
null, // unknown
null, // unknown
null // InteractionPlacementInfo
)!!
completable::class.java.methods.first {
it.name == "subscribe" && it.parameterTypes.isEmpty()
}.invoke(completable)
} }
} }

View File

@ -18,44 +18,44 @@ import me.rhunk.snapenhance.core.wrapper.impl.Message
import me.rhunk.snapenhance.core.wrapper.impl.MessageContent import me.rhunk.snapenhance.core.wrapper.impl.MessageContent
import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.mapper.impl.ViewBinderMapper
import java.nio.ByteBuffer import java.nio.ByteBuffer
class EventDispatcher( class EventDispatcher(
private val context: ModContext private val context: ModContext
) : Manager { ) : Manager {
private fun findClass(name: String) = context.androidContext.classLoader.loadClass(name)
private fun hookViewBinder() { private fun hookViewBinder() {
val cachedHooks = mutableListOf<String>() context.mappings.useMapper(ViewBinderMapper::class) {
val viewBinderMappings = runCatching { context.mappings.getMappedMap("ViewBinder") }.getOrNull() ?: return val cachedHooks = mutableListOf<String>()
fun cacheHook(clazz: Class<*>, block: Class<*>.() -> Unit) {
fun cacheHook(clazz: Class<*>, block: Class<*>.() -> Unit) { if (!cachedHooks.contains(clazz.name)) {
if (!cachedHooks.contains(clazz.name)) { clazz.block()
clazz.block() cachedHooks.add(clazz.name)
cachedHooks.add(clazz.name) }
} }
}
findClass(viewBinderMappings["class"].toString()).hookConstructor(HookStage.AFTER) { methodParam -> classReference.get()?.hookConstructor(HookStage.AFTER) { methodParam ->
cacheHook( cacheHook(
methodParam.thisObject<Any>()::class.java methodParam.thisObject<Any>()::class.java
) { ) {
hook(viewBinderMappings["bindMethod"].toString(), HookStage.AFTER) bindViewMethod@{ param -> hook(bindMethod.get().toString(), HookStage.AFTER) bindViewMethod@{ param ->
val instance = param.thisObject<Any>() val instance = param.thisObject<Any>()
val view = instance::class.java.methods.first { val view = instance::class.java.methods.firstOrNull {
it.name == viewBinderMappings["getViewMethod"].toString() it.name == getViewMethod.get().toString()
}.invoke(instance) as? View ?: return@bindViewMethod }?.invoke(instance) as? View ?: return@bindViewMethod
context.event.post( context.event.post(
BindViewEvent( BindViewEvent(
prevModel = param.arg(0), prevModel = param.arg(0),
nextModel = param.argNullable(1), nextModel = param.argNullable(1),
view = view view = view
)
) )
) }
} }
} }
} }
} }

View File

@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.util.hook.Hooker
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.mapper.impl.CompositeConfigurationProviderMapper
data class ConfigKeyInfo( data class ConfigKeyInfo(
val category: String?, val category: String?,
@ -23,128 +24,129 @@ data class ConfigFilter(
class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) { class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) {
override fun init() { override fun init() {
val compositeConfigurationProviderMappings = context.mappings.getMappedMap("CompositeConfigurationProvider") ?: throw Exception("Failed to get compositeConfigurationProviderMappings") context.mappings.useMapper(CompositeConfigurationProviderMapper::class) {
val enumMappings = compositeConfigurationProviderMappings["enum"] as Map<*, *> fun getConfigKeyInfo(key: Any?) = runCatching {
if (key == null) return@runCatching null
val keyClassMethods = key::class.java.methods
val keyName = keyClassMethods.firstOrNull { it.name == "getName" }?.invoke(key)?.toString() ?: key.toString()
val category = keyClassMethods.firstOrNull { it.name == configEnumMapping["getCategory"]?.get().toString() }?.invoke(key)?.toString() ?: return null
val valueHolder = keyClassMethods.firstOrNull { it.name == configEnumMapping["getValue"]?.get().toString() }?.invoke(key) ?: return null
val defaultValue = valueHolder.getObjectField(configEnumMapping["defaultValueField"]?.get().toString()) ?: return null
ConfigKeyInfo(category, keyName, defaultValue)
}.onFailure {
context.log.error("Failed to get config key info", it)
}.getOrNull()
fun getConfigKeyInfo(key: Any?) = runCatching { val propertyOverrides = mutableMapOf<String, ConfigFilter>()
if (key == null) return@runCatching null
val keyClassMethods = key::class.java.methods
val keyName = keyClassMethods.firstOrNull { it.name == "getName" }?.invoke(key)?.toString() ?: key.toString()
val category = keyClassMethods.firstOrNull { it.name == enumMappings["getCategory"].toString() }?.invoke(key)?.toString() ?: return null
val valueHolder = keyClassMethods.firstOrNull { it.name == enumMappings["getValue"].toString() }?.invoke(key) ?: return null
val defaultValue = valueHolder.getObjectField(enumMappings["defaultValueField"].toString()) ?: return null
ConfigKeyInfo(category, keyName, defaultValue)
}.onFailure {
context.log.error("Failed to get config key info", it)
}.getOrNull()
val propertyOverrides = mutableMapOf<String, ConfigFilter>() fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: (ConfigKeyInfo) -> Any?, isAppExperiment: Boolean = false) {
propertyOverrides[key] = ConfigFilter(filter, value, isAppExperiment)
fun overrideProperty(key: String, filter: (ConfigKeyInfo) -> Boolean, value: (ConfigKeyInfo) -> Any?, isAppExperiment: Boolean = false) {
propertyOverrides[key] = ConfigFilter(filter, value, isAppExperiment)
}
overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() },
{ true })
overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.forceUploadSourceQuality.get() },
{ true }, isAppExperiment = true)
overrideProperty("CAMERA_ME_ENABLE_HEVC_RECORDING", { context.config.camera.hevcRecording.get() },
{ true })
overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() },
{ true })
overrideProperty("REDUCE_MY_PROFILE_UI_COMPLEXITY", { context.config.userInterface.mapFriendNameTags.get() },
{ true })
overrideProperty("ENABLE_LONG_SNAP_SENDING", { context.config.global.disableSnapSplitting.get() },
{ true })
overrideProperty("DF_VOPERA_FOR_STORIES", { context.config.userInterface.verticalStoryViewer.get() },
{ true }, isAppExperiment = true)
overrideProperty("SPOTLIGHT_5TH_TAB_ENABLED", { context.config.userInterface.disableSpotlight.get() },
{ false })
overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.global.blockAds.get() },
{ true })
arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL", "INIT_PRIMARY_URL", "INIT_SHADOW_URL").forEach {
overrideProperty(it, { context.config.global.blockAds.get() }, { "http://127.0.0.1" })
}
findClass(compositeConfigurationProviderMappings["class"].toString()).hook(
compositeConfigurationProviderMappings["getProperty"].toString(),
HookStage.AFTER
) { param ->
val propertyKey = getConfigKeyInfo(param.argNullable<Any>(0)) ?: return@hook
propertyOverrides[propertyKey.name]?.let { (filter, value) ->
if (!filter(propertyKey)) return@let
param.setResult(value(propertyKey))
}
}
findClass(compositeConfigurationProviderMappings["class"].toString()).hook(
compositeConfigurationProviderMappings["observeProperty"].toString(),
HookStage.BEFORE
) { param ->
val enumData = param.arg<Any>(0)
val key = enumData.toString()
val setValue: (Any?) -> Unit = { value ->
val valueHolder = XposedHelpers.callMethod(enumData, enumMappings["getValue"].toString())
valueHolder.setObjectField(enumMappings["defaultValueField"].toString(), value)
} }
propertyOverrides[key]?.let { (filter, value) -> overrideProperty("STREAK_EXPIRATION_INFO", { context.config.userInterface.streakExpirationInfo.get() },
val keyInfo = getConfigKeyInfo(enumData) ?: return@let { true })
if (!filter(keyInfo)) return@let overrideProperty("TRANSCODING_MAX_QUALITY", { context.config.global.forceUploadSourceQuality.get() },
setValue(value(keyInfo)) { true }, isAppExperiment = true)
}
}
runCatching { overrideProperty("CAMERA_ME_ENABLE_HEVC_RECORDING", { context.config.camera.hevcRecording.get() },
val appExperimentProviderMappings = compositeConfigurationProviderMappings["appExperimentProvider"] as Map<*, *> { true })
val customBooleanPropertyRules = mutableListOf<(ConfigKeyInfo) -> Boolean>() overrideProperty("MEDIA_RECORDER_MAX_QUALITY_LEVEL", { context.config.camera.forceCameraSourceEncoding.get() },
{ true })
overrideProperty("REDUCE_MY_PROFILE_UI_COMPLEXITY", { context.config.userInterface.mapFriendNameTags.get() },
{ true })
overrideProperty("ENABLE_LONG_SNAP_SENDING", { context.config.global.disableSnapSplitting.get() },
{ true })
findClass(appExperimentProviderMappings["GetBooleanAppExperimentClass"].toString()).hook("invoke", HookStage.BEFORE) { param -> overrideProperty("DF_VOPERA_FOR_STORIES", { context.config.userInterface.verticalStoryViewer.get() },
val keyInfo = getConfigKeyInfo(param.arg(1)) ?: return@hook { true }, isAppExperiment = true)
if (customBooleanPropertyRules.any { it(keyInfo) }) { overrideProperty("SPOTLIGHT_5TH_TAB_ENABLED", { context.config.userInterface.disableSpotlight.get() },
param.setResult(true) { false })
return@hook
} overrideProperty("BYPASS_AD_FEATURE_GATE", { context.config.global.blockAds.get() },
propertyOverrides[keyInfo.name]?.let { (filter, value, isAppExperiment) -> { true })
if (!isAppExperiment || !filter(keyInfo)) return@let arrayOf("CUSTOM_AD_TRACKER_URL", "CUSTOM_AD_INIT_SERVER_URL", "CUSTOM_AD_SERVER_URL", "INIT_PRIMARY_URL", "INIT_SHADOW_URL").forEach {
param.setResult(value(keyInfo)) overrideProperty(it, { context.config.global.blockAds.get() }, { "http://127.0.0.1" })
}
} }
Hooker.ephemeralHookConstructor( classReference.getAsClass()?.hook(
findClass(compositeConfigurationProviderMappings["class"].toString()), getProperty.getAsString()!!,
HookStage.AFTER HookStage.AFTER
) { constructorParam -> ) { param ->
val instance = constructorParam.thisObject<Any>() val propertyKey = getConfigKeyInfo(param.argNullable<Any>(0)) ?: return@hook
val appExperimentProviderInstance = instance::class.java.fields.firstOrNull {
findClass(appExperimentProviderMappings["class"].toString()).isAssignableFrom(it.type)
}?.get(instance) ?: return@ephemeralHookConstructor
appExperimentProviderInstance::class.java.methods.first { propertyOverrides[propertyKey.name]?.let { (filter, value) ->
it.name == appExperimentProviderMappings["hasExperimentMethod"].toString() if (!filter(propertyKey)) return@let
}.hook(HookStage.BEFORE) { param -> param.setResult(value(propertyKey))
val keyInfo = getConfigKeyInfo(param.arg(0)) ?: return@hook }
if (customBooleanPropertyRules.any { it(keyInfo) }) { }
param.setResult(true)
return@hook classReference.get()?.hook(
observeProperty.getAsString()!!,
HookStage.BEFORE
) { param ->
val enumData = param.arg<Any>(0)
val key = enumData.toString()
val setValue: (Any?) -> Unit = { value ->
val valueHolder = XposedHelpers.callMethod(enumData, configEnumMapping["getValue"]?.getAsString())
valueHolder.setObjectField(configEnumMapping["defaultValueField"]?.getAsString()!!, value)
}
propertyOverrides[key]?.let { (filter, value) ->
val keyInfo = getConfigKeyInfo(enumData) ?: return@let
if (!filter(keyInfo)) return@let
setValue(value(keyInfo))
}
}
runCatching {
val customBooleanPropertyRules = mutableListOf<(ConfigKeyInfo) -> Boolean>()
appExperimentProvider["getBooleanAppExperimentClass"]?.getAsClass()
?.hook("invoke", HookStage.BEFORE) { param ->
val keyInfo = getConfigKeyInfo(param.arg(1)) ?: return@hook
if (customBooleanPropertyRules.any { it(keyInfo) }) {
param.setResult(true)
return@hook
}
propertyOverrides[keyInfo.name]?.let { (filter, value, isAppExperiment) ->
if (!isAppExperiment || !filter(keyInfo)) return@let
param.setResult(value(keyInfo))
}
} }
val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook
if (propertyOverride.isAppExperiment && propertyOverride.filter(keyInfo)) param.setResult(true)
}
}
if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) {
customBooleanPropertyRules.add { key -> Hooker.ephemeralHookConstructor(
key.category == "PLUS" && key.defaultValue is Boolean && key.name?.endsWith("_GATE") == true classReference.get()!!,
HookStage.AFTER
) { constructorParam ->
val instance = constructorParam.thisObject<Any>()
val appExperimentProviderInstance = instance::class.java.fields.firstOrNull {
appExperimentProvider["class"]?.getAsClass()?.isAssignableFrom(it.type) == true
}?.get(instance) ?: return@ephemeralHookConstructor
appExperimentProviderInstance::class.java.methods.first {
it.name == appExperimentProvider["hasExperimentMethod"]?.getAsString().toString()
}.hook(HookStage.BEFORE) { param ->
val keyInfo = getConfigKeyInfo(param.arg(0)) ?: return@hook
if (customBooleanPropertyRules.any { it(keyInfo) }) {
param.setResult(true)
return@hook
}
val propertyOverride = propertyOverrides[keyInfo.name] ?: return@hook
if (propertyOverride.isAppExperiment && propertyOverride.filter(keyInfo)) param.setResult(true)
}
} }
if (context.config.experimental.hiddenSnapchatPlusFeatures.get()) {
customBooleanPropertyRules.add { key ->
key.category == "PLUS" && key.defaultValue is Boolean && key.name?.endsWith("_GATE") == true
}
}
}.onFailure {
context.log.error("Failed to hook appExperimentProvider", it)
} }
}.onFailure {
context.log.error("Failed to hook appExperimentProvider", it)
} }
} }
} }

View File

@ -4,6 +4,7 @@ import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.mapper.impl.OperaViewerParamsMapper
class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
data class OverrideKey( data class OverrideKey(
@ -17,7 +18,6 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam
) )
override fun onActivityCreate() { override fun onActivityCreate() {
val operaViewerParamsMappings = context.mappings.getMappedMap("OperaViewerParams") ?: throw Exception("Failed to get operaViewerParamsMappings")
val overrideMap = mutableMapOf<String, Override>() val overrideMap = mutableMapOf<String, Override>()
fun overrideParam(key: String, filter: (value: Any?) -> Boolean, value: (overrideKey: OverrideKey, value: Any?) -> Any?) { fun overrideParam(key: String, filter: (value: Any?) -> Boolean, value: (overrideKey: OverrideKey, value: Any?) -> Any?) {
@ -36,26 +36,28 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam
}) })
} }
findClass(operaViewerParamsMappings["class"].toString()).hook(operaViewerParamsMappings["putMethod"].toString(), HookStage.BEFORE) { param -> context.mappings.useMapper(OperaViewerParamsMapper::class) {
val key = param.argNullable<Any>(0)?.let { key -> classReference.get()?.hook(putMethod.get()!!, HookStage.BEFORE) { param ->
val fields = key::class.java.fields val key = param.argNullable<Any>(0)?.let { key ->
OverrideKey( val fields = key::class.java.fields
name = fields.firstOrNull { OverrideKey(
it.type == String::class.java name = fields.firstOrNull {
}?.get(key)?.toString() ?: return@hook, it.type == String::class.java
defaultValue = fields.firstOrNull { }?.get(key)?.toString() ?: return@hook,
it.type == Object::class.java defaultValue = fields.firstOrNull {
}?.get(key) it.type == Object::class.java
) }?.get(key)
} ?: return@hook )
val value = param.argNullable<Any>(1) ?: return@hook } ?: return@hook
val value = param.argNullable<Any>(1) ?: return@hook
overrideMap[key.name]?.let { override -> overrideMap[key.name]?.let { override ->
if (override.filter(value)) { if (override.filter(value)) {
runCatching { runCatching {
param.setArg(1, override.value(key, value)) param.setArg(1, override.value(key, value))
}.onFailure { }.onFailure {
context.log.error("Failed to override param $key", it) context.log.error("Failed to override param $key", it)
}
} }
} }
} }

View File

@ -47,6 +47,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.media.dash.SnapPlaylistItem
import me.rhunk.snapenhance.core.wrapper.impl.media.opera.Layer import me.rhunk.snapenhance.core.wrapper.impl.media.opera.Layer
import me.rhunk.snapenhance.core.wrapper.impl.media.opera.ParamMap import me.rhunk.snapenhance.core.wrapper.impl.media.opera.ParamMap
import me.rhunk.snapenhance.core.wrapper.impl.media.toKeyPair import me.rhunk.snapenhance.core.wrapper.impl.media.toKeyPair
import me.rhunk.snapenhance.mapper.impl.OperaPageViewControllerMapper
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.nio.file.Paths import java.nio.file.Paths
import java.util.UUID import java.util.UUID
@ -462,51 +463,50 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
} }
override fun asyncOnActivityCreate() { override fun asyncOnActivityCreate() {
val operaViewerControllerClass: Class<*> = context.mappings.getMappedClass("OperaPageViewController", "class") context.mappings.useMapper(OperaPageViewControllerMapper::class) {
val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param ->
val viewState = (param.thisObject() as Any).getObjectField(viewStateField.get()!!).toString()
if (viewState != "FULLY_DISPLAYED") {
return@onOperaViewStateCallback
}
val operaLayerList = (param.thisObject() as Any).getObjectField(layerListField.get()!!) as ArrayList<*>
val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap
val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param -> if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list"))
return@onOperaViewStateCallback
val viewState = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "viewStateField")!!).toString() val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>()
if (viewState != "FULLY_DISPLAYED") { val isVideo = mediaParamMap.containsKey("video_media_info_list")
return@onOperaViewStateCallback
}
val operaLayerList = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "layerListField")!!) as ArrayList<*>
val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap
if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list")) mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo(
return@onOperaViewStateCallback (if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!!
)
val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>() if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) {
val isVideo = mediaParamMap.containsKey("video_media_info_list") mediaInfoMap[SplitMediaAssetType.OVERLAY] =
MediaInfo(mediaParamMap["overlay_image_media_info"]!!)
}
lastSeenMapParams = mediaParamMap
lastSeenMediaInfoMap = mediaInfoMap
mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo( if (!canAutoDownload()) return@onOperaViewStateCallback
(if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!!
)
if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) { context.executeAsync {
mediaInfoMap[SplitMediaAssetType.OVERLAY] = runCatching {
MediaInfo(mediaParamMap["overlay_image_media_info"]!!) handleOperaMedia(mediaParamMap, mediaInfoMap, false)
} }.onFailure {
lastSeenMapParams = mediaParamMap context.log.error("Failed to handle opera media", it)
lastSeenMediaInfoMap = mediaInfoMap context.longToast(it.message)
}
if (!canAutoDownload()) return@onOperaViewStateCallback
context.executeAsync {
runCatching {
handleOperaMedia(mediaParamMap, mediaInfoMap, false)
}.onFailure {
context.log.error("Failed to handle opera media", it)
context.longToast(it.message)
} }
} }
}
arrayOf("onDisplayStateChange", "onDisplayStateChangeGesture").forEach { methodName -> arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName ->
operaViewerControllerClass.hook( classReference.get()?.hook(
context.mappings.getMappedValue("OperaPageViewController", methodName) ?: return@forEach, methodName.get() ?: return@forEach,
HookStage.AFTER, onOperaViewStateCallback HookStage.AFTER, onOperaViewStateCallback
) )
}
} }
} }

View File

@ -5,61 +5,61 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper
class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
var friendRelationshipChangerInstance: Any? = null var friendRelationshipChangerInstance: Any? = null
private set private set
override fun onActivityCreate() { override fun onActivityCreate() {
val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger") ?: throw Exception("Failed to get friendRelationshipChangerMapping") context.mappings.useMapper(FriendRelationshipChangerMapper::class) {
classReference.get()?.hookConstructor(HookStage.AFTER) { param ->
findClass(friendRelationshipChangerMapping["class"].toString()).hookConstructor(HookStage.AFTER) { param -> friendRelationshipChangerInstance = param.thisObject()
friendRelationshipChangerInstance = param.thisObject()
}
findClass(friendRelationshipChangerMapping["class"].toString())
.hook(friendRelationshipChangerMapping["addFriendMethod"].toString(), HookStage.BEFORE) { param ->
val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@hook
fun setEnum(index: Int, value: String) {
val enumData = param.arg<Any>(index)
enumData::class.java.enumConstants.first { it.toString() == value }.let {
param.setArg(index, it)
}
} }
when (spoofedSource) { classReference.get()?.hook(addFriendMethod.get()!!, HookStage.BEFORE) { param ->
"added_by_quick_add" -> { val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@hook
setEnum(1, "PROFILE")
setEnum(2, "ADD_FRIENDS_BUTTON_ON_TOP_BAR_ON_FRIENDS_FEED") fun setEnum(index: Int, value: String) {
setEnum(3, "ADDED_BY_SUGGESTED") val enumData = param.arg<Any>(index)
enumData::class.java.enumConstants.first { it.toString() == value }.let {
param.setArg(index, it)
}
} }
"added_by_group_chat" -> {
setEnum(1, "PROFILE") when (spoofedSource) {
setEnum(2, "GROUP_PROFILE") "added_by_quick_add" -> {
setEnum(3, "ADDED_BY_GROUP_CHAT") setEnum(1, "PROFILE")
setEnum(2, "ADD_FRIENDS_BUTTON_ON_TOP_BAR_ON_FRIENDS_FEED")
setEnum(3, "ADDED_BY_SUGGESTED")
}
"added_by_group_chat" -> {
setEnum(1, "PROFILE")
setEnum(2, "GROUP_PROFILE")
setEnum(3, "ADDED_BY_GROUP_CHAT")
}
"added_by_username" -> {
setEnum(1, "SEARCH")
setEnum(2, "SEARCH")
setEnum(3, "ADDED_BY_USERNAME")
}
"added_by_qr_code" -> {
setEnum(1, "PROFILE")
setEnum(2, "PROFILE")
setEnum(3, "ADDED_BY_QR_CODE")
}
"added_by_mention" -> {
setEnum(1, "CONTEXT_CARDS")
setEnum(2, "CONTEXT_CARD")
setEnum(3, "ADDED_BY_MENTION")
}
"added_by_community" -> {
setEnum(1, "PROFILE")
setEnum(2, "PROFILE")
setEnum(3, "ADDED_BY_COMMUNITY")
}
else -> return@hook
} }
"added_by_username" -> {
setEnum(1, "SEARCH")
setEnum(2, "SEARCH")
setEnum(3, "ADDED_BY_USERNAME")
}
"added_by_qr_code" -> {
setEnum(1, "PROFILE")
setEnum(2, "PROFILE")
setEnum(3, "ADDED_BY_QR_CODE")
}
"added_by_mention" -> {
setEnum(1, "CONTEXT_CARDS")
setEnum(2, "CONTEXT_CARD")
setEnum(3, "ADDED_BY_MENTION")
}
"added_by_community" -> {
setEnum(1, "PROFILE")
setEnum(2, "PROFILE")
setEnum(3, "ADDED_BY_COMMUNITY")
}
else -> return@hook
} }
} }
} }

View File

@ -4,18 +4,20 @@ import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.mapper.impl.StoryBoostStateMapper
class InfiniteStoryBoost : Feature("InfiniteStoryBoost", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { class InfiniteStoryBoost : Feature("InfiniteStoryBoost", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() { override fun asyncOnActivityCreate() {
if (!context.config.experimental.infiniteStoryBoost.get()) return if (!context.config.experimental.infiniteStoryBoost.get()) return
val storyBoostStateClass = context.mappings.getMappedClass("StoryBoostStateClass") ?: throw Exception("Failed to get storyBoostStateClass")
storyBoostStateClass.hookConstructor(HookStage.BEFORE) { param -> context.mappings.useMapper(StoryBoostStateMapper::class) {
val startTimeMillis = param.arg<Long>(1) classReference.get()?.hookConstructor(HookStage.BEFORE) { param ->
//reset timestamp if it's more than 24 hours val startTimeMillis = param.arg<Long>(1)
if (System.currentTimeMillis() - startTimeMillis > 86400000) { //reset timestamp if it's more than 24 hours
param.setArg(1, 0) if (System.currentTimeMillis() - startTimeMillis > 86400000) {
param.setArg(2, 0) param.setArg(1, 0)
param.setArg(2, 0)
}
} }
} }
} }

View File

@ -3,20 +3,21 @@ package me.rhunk.snapenhance.core.features.impl.experiments
import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.mapper.impl.BCryptClassMapper
class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() { override fun asyncOnActivityCreate() {
val bcrypt = context.mappings.getMappedMap("BCrypt") ?: throw Exception("Failed to get bcrypt mappings") if (!context.config.experimental.meoPasscodeBypass.get()) return
Hooker.hook( context.mappings.useMapper(BCryptClassMapper::class) {
context.androidContext.classLoader.loadClass(bcrypt["class"].toString()), classReference.get()?.hook(
bcrypt["hashMethod"].toString(), hashMethod.get()!!,
HookStage.BEFORE, HookStage.BEFORE,
{ context.config.experimental.meoPasscodeBypass.get() }, ) { param ->
) { param -> //set the hash to the result of the method
//set the hash to the result of the method param.setResult(param.arg(1))
param.setResult(param.arg(1)) }
} }
} }
} }

View File

@ -4,17 +4,19 @@ import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.mapper.impl.ScoreUpdateMapper
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
class NoFriendScoreDelay : Feature("NoFriendScoreDelay", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { class NoFriendScoreDelay : Feature("NoFriendScoreDelay", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
override fun onActivityCreate() { override fun onActivityCreate() {
if (!context.config.experimental.noFriendScoreDelay.get()) return if (!context.config.experimental.noFriendScoreDelay.get()) return
val scoreUpdateClass = context.mappings.getMappedClass("ScoreUpdate") ?: throw Exception("Failed to get scoreUpdateClass")
scoreUpdateClass.hookConstructor(HookStage.BEFORE) { param -> context.mappings.useMapper(ScoreUpdateMapper::class) {
val constructor = param.method() as Constructor<*> classReference.get()?.hookConstructor(HookStage.BEFORE) { param ->
if (constructor.parameterTypes.size < 3 || constructor.parameterTypes[3] != java.util.Collection::class.java) return@hookConstructor val constructor = param.method() as Constructor<*>
param.setArg(2, 0L) if (constructor.parameterTypes.size < 3 || constructor.parameterTypes[3] != java.util.Collection::class.java) return@hookConstructor
param.setArg(2, 0L)
}
} }
} }
} }

View File

@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.mapper.impl.DefaultMediaItemMapper
import java.io.File import java.io.File
class BypassVideoLengthRestriction : class BypassVideoLengthRestriction :
@ -45,21 +46,23 @@ class BypassVideoLengthRestriction :
} }
} }
context.mappings.getMappedClass("DefaultMediaItem") context.mappings.useMapper(DefaultMediaItemMapper::class) {
?.hookConstructor(HookStage.BEFORE) { param -> defaultMediaItem.getAsClass()?.hookConstructor(HookStage.BEFORE) { param ->
//set the video length argument //set the video length argument
param.setArg(5, -1L) param.setArg(5, -1L)
} }
}
} }
//TODO: allow split from any source //TODO: allow split from any source
if (mode == "split") { if (mode == "split") {
val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId") ?: throw Exception("Failed to get cameraRollId mappings")
// memories grid // memories grid
findClass(cameraRollId["class"].toString()).hookConstructor(HookStage.AFTER) { param -> context.mappings.useMapper(DefaultMediaItemMapper::class) {
//set the durationMs field cameraRollMediaId.getAsClass()?.hookConstructor(HookStage.AFTER) { param ->
param.thisObject<Any>() //set the durationMs field
.setObjectField(cameraRollId["durationMsField"].toString(), -1L) param.thisObject<Any>()
.setObjectField(durationMsField.get()!!, -1L)
}
} }
// chat camera roll grid // chat camera roll grid

View File

@ -4,20 +4,20 @@ import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.mapper.impl.MediaQualityLevelProviderMapper
import java.lang.reflect.Method
class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) { class MediaQualityLevelOverride : Feature("MediaQualityLevelOverride", loadParams = FeatureLoadParams.INIT_SYNC) {
override fun init() { override fun init() {
val enumQualityLevel = context.mappings.getMappedClass("EnumQualityLevel") ?: throw Exception("Failed to get enumQualityLevelMappings") if (!context.config.global.forceUploadSourceQuality.get()) return
val mediaQualityLevelProvider = context.mappings.getMappedMap("MediaQualityLevelProvider") ?: throw Exception("Failed to get mediaQualityLevelProviderMappings")
val forceMediaSourceQuality by context.config.global.forceUploadSourceQuality context.mappings.useMapper(MediaQualityLevelProviderMapper::class) {
mediaQualityLevelProvider.getAsClass()?.hook(
context.androidContext.classLoader.loadClass(mediaQualityLevelProvider["class"].toString()).hook( mediaQualityLevelProviderMethod.getAsString()!!,
mediaQualityLevelProvider["method"].toString(), HookStage.BEFORE
HookStage.BEFORE, ) { param ->
{ forceMediaSourceQuality } param.setResult((param.method() as Method).returnType.enumConstants.firstOrNull { it.toString() == "LEVEL_MAX" } )
) { param -> }
param.setResult(enumQualityLevel.enumConstants.firstOrNull { it.toString() == "LEVEL_MAX" } )
} }
} }
} }

View File

@ -3,8 +3,9 @@ package me.rhunk.snapenhance.core.features.impl.global
import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.Hooker
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.mapper.impl.PlusSubscriptionMapper
class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_SYNC) { class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_SYNC) {
private val originalSubscriptionTime = (System.currentTimeMillis() - 7776000000L) private val originalSubscriptionTime = (System.currentTimeMillis() - 7776000000L)
@ -13,17 +14,17 @@ class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_
override fun init() { override fun init() {
if (!context.config.global.snapchatPlus.get()) return if (!context.config.global.snapchatPlus.get()) return
val subscriptionInfoClass = context.mappings.getMappedClass("SubscriptionInfoClass") ?: throw Exception("Failed to get subscriptionInfoClass") context.mappings.useMapper(PlusSubscriptionMapper::class) {
classReference.get()?.hookConstructor(HookStage.BEFORE) { param ->
if (param.arg<Int>(0) == 2) return@hookConstructor
//subscription tier
param.setArg(0, 2)
//subscription status
param.setArg(1, 2)
Hooker.hookConstructor(subscriptionInfoClass, HookStage.BEFORE) { param -> param.setArg(2, originalSubscriptionTime)
if (param.arg<Int>(0) == 2) return@hookConstructor param.setArg(3, expirationTimeMillis)
//subscription tier }
param.setArg(0, 2)
//subscription status
param.setArg(1, 2)
param.setArg(2, originalSubscriptionTime)
param.setArg(3, expirationTimeMillis)
} }
// optional as ConfigurationOverride does this too // optional as ConfigurationOverride does this too

View File

@ -13,6 +13,7 @@ import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.Message
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
import java.util.concurrent.Executors import java.util.concurrent.Executors
class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
@ -70,19 +71,21 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE,
override fun asyncOnActivityCreate() { override fun asyncOnActivityCreate() {
// called when enter in a conversation // called when enter in a conversation
context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback").hook( context.mappings.useMapper(CallbackMapper::class) {
"onFetchConversationWithMessagesComplete", callbacks.getClass("FetchConversationWithMessagesCallback")?.hook(
HookStage.BEFORE, "onFetchConversationWithMessagesComplete",
{ autoSaveFilter.isNotEmpty() } HookStage.BEFORE,
) { param -> { autoSaveFilter.isNotEmpty() }
val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!) ) { param ->
if (!canSaveInConversation(conversationId.toString())) return@hook val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!)
if (!canSaveInConversation(conversationId.toString())) return@hook
val messages = param.arg<List<Any>>(1).map { Message(it) } val messages = param.arg<List<Any>>(1).map { Message(it) }
messages.forEach { messages.forEach {
if (!canSaveMessage(it)) return@forEach if (!canSaveMessage(it)) return@forEach
asyncSaveExecutorService.submit { asyncSaveExecutorService.submit {
saveMessage(conversationId.toString(), it) saveMessage(conversationId.toString(), it)
}
} }
} }
} }

View File

@ -18,6 +18,8 @@ import me.rhunk.snapenhance.core.wrapper.impl.Message
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter
import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
import me.rhunk.snapenhance.mapper.impl.FriendsFeedEventDispatcherMapper
import java.util.concurrent.Future import java.util.concurrent.Future
class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) {
@ -47,24 +49,28 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
} }
} }
context.mappings.getMappedClass("callbacks", "ConversationManagerDelegate").apply { context.mappings.useMapper(CallbackMapper::class) {
hookConstructor(HookStage.AFTER) { param -> callbacks.getClass("ConversationManagerDelegate")?.apply {
conversationManagerDelegate = param.thisObject() hookConstructor(HookStage.AFTER) { param ->
} conversationManagerDelegate = param.thisObject()
hook("onConversationUpdated", HookStage.BEFORE) { param -> }
context.event.post(ConversationUpdateEvent( hook("onConversationUpdated", HookStage.BEFORE) { param ->
conversationId = SnapUUID(param.arg(0)).toString(), context.event.post(ConversationUpdateEvent(
conversation = param.argNullable(1), conversationId = SnapUUID(param.arg(0)).toString(),
messages = param.arg<ArrayList<*>>(2).map { Message(it) }, conversation = param.argNullable(1),
).apply { adapter = param }) { messages = param.arg<ArrayList<*>>(2).map { Message(it) },
param.setArg(2, messages.map { it.instanceNonNull() }.toCollection(ArrayList())) ).apply { adapter = param }) {
param.setArg(
2,
messages.map { it.instanceNonNull() }.toCollection(ArrayList())
)
}
} }
} }
} callbacks.getClass("IdentityDelegate")?.apply {
hookConstructor(HookStage.AFTER) {
context.mappings.getMappedClass("callbacks", "IdentityDelegate").apply { identityDelegate = it.thisObject()
hookConstructor(HookStage.AFTER) { }
identityDelegate = it.thisObject()
} }
} }
} }
@ -96,10 +102,10 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
} }
override fun onActivityCreate() { override fun onActivityCreate() {
context.mappings.getMappedObjectNullable("FriendsFeedEventDispatcher").let { it as? Map<*, *> }?.let { mappings -> context.mappings.useMapper(FriendsFeedEventDispatcherMapper::class) {
findClass(mappings["class"].toString()).hook("onItemLongPress", HookStage.BEFORE) { param -> classReference.getAsClass()?.hook("onItemLongPress", HookStage.BEFORE) { param ->
val viewItemContainer = param.arg<Any>(0) val viewItemContainer = param.arg<Any>(0)
val viewItem = viewItemContainer.getObjectField(mappings["viewModelField"].toString()).toString() val viewItem = viewItemContainer.getObjectField(viewModelField.get()!!).toString()
val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also { val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also {
if (it.startsWith("null")) return@hook if (it.startsWith("null")) return@hook
} }
@ -109,7 +115,6 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
} }
} }
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
val instance = param.thisObject<Any>() val instance = param.thisObject<Any>()
val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor
@ -122,17 +127,21 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
}.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId } }.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId }
} }
context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback").hook("onSuccess", HookStage.BEFORE) { param -> context.mappings.useMapper(CallbackMapper::class) {
val userIdToConversation = (param.arg<ArrayList<*>>(0)) callbacks.getClass("GetOneOnOneConversationIdsCallback")?.hook("onSuccess", HookStage.BEFORE) { param ->
.takeIf { it.isNotEmpty() } val userIdToConversation = (param.arg<ArrayList<*>>(0))
?.get(0) ?: return@hook .takeIf { it.isNotEmpty() }
?.get(0) ?: return@hook
lastFetchConversationUUID = SnapUUID(userIdToConversation.getObjectField("mConversationId")) lastFetchConversationUUID =
lastFetchConversationUserUUID = SnapUUID(userIdToConversation.getObjectField("mUserId")) SnapUUID(userIdToConversation.getObjectField("mConversationId"))
lastFetchConversationUserUUID =
SnapUUID(userIdToConversation.getObjectField("mUserId"))
}
} }
with(context.classCache.conversationManager) { context.classCache.conversationManager.apply {
Hooker.hook(this, "enterConversation", HookStage.BEFORE) { param -> hook("enterConversation", HookStage.BEFORE) { param ->
openedConversationUUID = SnapUUID(param.arg(0)) openedConversationUUID = SnapUUID(param.arg(0))
if (context.config.messaging.bypassMessageRetentionPolicy.get()) { if (context.config.messaging.bypassMessageRetentionPolicy.get()) {
val callback = param.argNullable<Any>(2) ?: return@hook val callback = param.argNullable<Any>(2) ?: return@hook
@ -141,7 +150,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
} }
} }
Hooker.hook(this, "exitConversation", HookStage.BEFORE) { hook("exitConversation", HookStage.BEFORE) {
openedConversationUUID = null openedConversationUUID = null
} }
} }

View File

@ -12,6 +12,7 @@ import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.core.util.ktx.getIdentifier import me.rhunk.snapenhance.core.util.ktx.getIdentifier
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -43,8 +44,8 @@ class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoa
presenceService = it.thisObject() presenceService = it.thisObject()
} }
context.mappings.getMappedClass("callbacks", "PresenceServiceDelegate") context.mappings.useMapper(CallbackMapper::class) {
.hook("notifyActiveConversationsChanged", HookStage.BEFORE) { callbacks.getClass("PresenceServiceDelegate")?.hook("notifyActiveConversationsChanged", HookStage.BEFORE) {
val activeConversations = presenceService::class.java.methods.find { it.name == "getActiveConversations" }?.invoke(presenceService) as? Map<*, *> ?: return@hook // conversationId, conversationInfo (this.mPeekingParticipants) val activeConversations = presenceService::class.java.methods.find { it.name == "getActiveConversations" }?.invoke(presenceService) as? Map<*, *> ?: return@hook // conversationId, conversationInfo (this.mPeekingParticipants)
if (activeConversations.isEmpty()) { if (activeConversations.isEmpty()) {
@ -75,6 +76,7 @@ class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoa
} }
peekingConversations[conversationId.toString()] = peekingParticipantsIds peekingConversations[conversationId.toString()] = peekingParticipantsIds
} }
}
} }
} }

View File

@ -17,6 +17,7 @@ import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.core.wrapper.impl.ScSize import me.rhunk.snapenhance.core.wrapper.impl.ScSize
import me.rhunk.snapenhance.mapper.impl.ScCameraSettingsMapper
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -62,19 +63,21 @@ class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACT
} }
} }
context.mappings.getMappedClass("ScCameraSettings")?.hookConstructor(HookStage.BEFORE) { param -> context.mappings.useMapper(ScCameraSettingsMapper::class) {
val previewResolution = ScSize(param.argNullable(2)) classReference.get()?.hookConstructor(HookStage.BEFORE) { param ->
val captureResolution = ScSize(param.argNullable(3)) val previewResolution = ScSize(param.argNullable(2))
val captureResolution = ScSize(param.argNullable(3))
if (previewResolution.isPresent() && captureResolution.isPresent()) { if (previewResolution.isPresent() && captureResolution.isPresent()) {
previewResolutionConfig?.let { previewResolutionConfig?.let {
previewResolution.first = it[0] previewResolution.first = it[0]
previewResolution.second = it[1] previewResolution.second = it[1]
} }
captureResolutionConfig?.let { captureResolutionConfig?.let {
captureResolution.first = it[0] captureResolution.first = it[0]
captureResolution.second = it[1] captureResolution.second = it[1]
}
} }
} }
} }

View File

@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED, loadParams = FeatureLoadParams.INIT_SYNC) { class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED, loadParams = FeatureLoadParams.INIT_SYNC) {
private fun createDeletedFeedEntry(conversationId: String) = context.gson.fromJson( private fun createDeletedFeedEntry(conversationId: String) = context.gson.fromJson(
@ -41,30 +42,31 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType
override fun init() { override fun init() {
if (!context.config.userInterface.hideFriendFeedEntry.get()) return if (!context.config.userInterface.hideFriendFeedEntry.get()) return
arrayOf( context.mappings.useMapper(CallbackMapper::class) {
"QueryFeedCallback" to "onQueryFeedComplete", arrayOf(
"FeedManagerDelegate" to "onFeedEntriesUpdated", "QueryFeedCallback" to "onQueryFeedComplete",
"FeedManagerDelegate" to "onInternalSyncFeed", "FeedManagerDelegate" to "onFeedEntriesUpdated",
"SyncFeedCallback" to "onSyncFeedComplete", "FeedManagerDelegate" to "onInternalSyncFeed",
).forEach { (callbackName, methodName) -> "SyncFeedCallback" to "onSyncFeedComplete",
context.mappings.getMappedClass("callbacks", callbackName) ).forEach { (callbackName, methodName) ->
.hook(methodName, HookStage.BEFORE) { param -> findClass(callbacks.get()!![callbackName] ?: return@forEach).hook(methodName, HookStage.BEFORE) { param ->
filterFriendFeed(param.arg(0)) filterFriendFeed(param.arg(0))
} }
}
context.mappings.getMappedClass("callbacks", "FetchAndSyncFeedCallback")
.hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param ->
val deletedConversations: ArrayList<Any> = param.arg(2)
filterFriendFeed(param.arg(0), deletedConversations)
if (deletedConversations.any {
val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString()
context.database.getFeedEntryByConversationId(uuid) != null
}) {
param.setArg(4, true)
}
} }
callbacks.getClass("FetchAndSyncFeedCallback")
?.hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param ->
val deletedConversations: ArrayList<Any> = param.arg(2)
filterFriendFeed(param.arg(0), deletedConversations)
if (deletedConversations.any {
val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString()
context.database.getFeedEntryByConversationId(uuid) != null
}) {
param.setArg(4, true)
}
}
}
} }
override fun getRuleState() = RuleState.WHITELIST override fun getRuleState() = RuleState.WHITELIST

View File

@ -5,17 +5,19 @@ import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hookConstructor import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.mapper.impl.FriendingDataSourcesMapper
class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
override fun onActivityCreate() { override fun onActivityCreate() {
if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return
val friendingDataSource = context.mappings.getMappedMap("FriendingDataSources") ?: throw Exception("Failed to get friendingDataSourceMappings") context.mappings.useMapper(FriendingDataSourcesMapper::class) {
findClass(friendingDataSource["class"].toString()).hookConstructor(HookStage.AFTER) { param -> classReference.getAsClass()?.hookConstructor(HookStage.AFTER) { param ->
param.thisObject<Any>().setObjectField( param.thisObject<Any>().setObjectField(
friendingDataSource["quickAddSourceListField"].toString(), quickAddSourceListField.get()!!,
arrayListOf<Any>() arrayListOf<Any>()
) )
}
} }
} }
} }

View File

@ -19,6 +19,7 @@ import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getDimens import me.rhunk.snapenhance.core.util.ktx.getDimens
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.util.media.PreviewUtils import me.rhunk.snapenhance.core.util.media.PreviewUtils
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
import java.io.File import java.io.File
class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) { class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
@ -29,17 +30,19 @@ class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_S
override fun init() { override fun init() {
if (!isEnabled) return if (!isEnabled) return
context.mappings.getMappedClass("callbacks", "ContentCallback").hook("handleContentResult", HookStage.BEFORE) { param -> context.mappings.useMapper(CallbackMapper::class) {
val contentResult = param.arg<Any>(0) callbacks.getClass("ContentCallback")?.hook("handleContentResult", HookStage.BEFORE) { param ->
val classMethods = contentResult::class.java.methods val contentResult = param.arg<Any>(0)
val classMethods = contentResult::class.java.methods
val contentKey = classMethods.find { it.name == "getContentKey" }?.invoke(contentResult) ?: return@hook val contentKey = classMethods.find { it.name == "getContentKey" }?.invoke(contentResult) ?: return@hook
if (contentKey.getObjectField("mMediaContextType").toString() != "CHAT") return@hook if (contentKey.getObjectField("mMediaContextType").toString() != "CHAT") return@hook
val filePath = classMethods.find { it.name == "getFilePath" }?.invoke(contentResult) ?: return@hook val filePath = classMethods.find { it.name == "getFilePath" }?.invoke(contentResult) ?: return@hook
val mediaId = contentKey.getObjectField("mMediaId").toString() val mediaId = contentKey.getObjectField("mMediaId").toString()
mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString()) mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString())
}
} }
} }

View File

@ -8,6 +8,7 @@ import me.rhunk.snapenhance.core.util.CallbackBuilder
import me.rhunk.snapenhance.core.wrapper.AbstractWrapper import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.core.wrapper.impl.MessageDestinations
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
class MessageSender( class MessageSender(
private val context: ModContext, private val context: ModContext,
@ -55,7 +56,13 @@ class MessageSender(
} }
private val sendMessageCallback by lazy { context.mappings.getMappedClass("callbacks", "SendMessageCallback") } private val sendMessageCallback by lazy {
lateinit var result: Class<*>
context.mappings.useMapper(CallbackMapper::class) {
result = callbacks.getClass("SendMessageCallback") ?: return@useMapper
}
result
}
private fun createLocalMessageContentTemplate( private fun createLocalMessageContentTemplate(
contentType: ContentType, contentType: ContentType,

View File

@ -6,6 +6,7 @@ import me.rhunk.snapenhance.core.util.CallbackBuilder
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.core.wrapper.AbstractWrapper import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
typealias CallbackResult = (error: String?) -> Unit typealias CallbackResult = (error: String?) -> Unit
@ -26,20 +27,29 @@ class ConversationManager(
private val getOneOnOneConversationIds by lazy { findMethodByName("getOneOnOneConversationIds") } private val getOneOnOneConversationIds by lazy { findMethodByName("getOneOnOneConversationIds") }
private fun getCallbackClass(name: String): Class<*> {
lateinit var result: Class<*>
context.mappings.useMapper(CallbackMapper::class) {
result = context.androidContext.classLoader.loadClass(callbacks.get()!![name])
}
return result
}
fun updateMessage(conversationId: String, messageId: Long, action: MessageUpdate, onResult: CallbackResult = {}) { fun updateMessage(conversationId: String, messageId: Long, action: MessageUpdate, onResult: CallbackResult = {}) {
updateMessageMethod.invoke( updateMessageMethod.invoke(
instanceNonNull(), instanceNonNull(),
SnapUUID.fromString(conversationId).instanceNonNull(), SnapUUID.fromString(conversationId).instanceNonNull(),
messageId, messageId,
context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == action.toString() }, context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == action.toString() },
CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) CallbackBuilder(getCallbackClass("Callback"))
.override("onSuccess") { onResult(null) } .override("onSuccess") { onResult(null) }
.override("onError") { onResult(it.arg<Any>(0).toString()) }.build() .override("onError") { onResult(it.arg<Any>(0).toString()) }.build()
) )
} }
fun fetchConversationWithMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int, onSuccess: (message: List<Message>) -> Unit, onError: (error: String) -> Unit) { fun fetchConversationWithMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int, onSuccess: (message: List<Message>) -> Unit, onError: (error: String) -> Unit) {
val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")) val callback = CallbackBuilder(getCallbackClass("FetchConversationWithMessagesCallback"))
.override("onFetchConversationWithMessagesComplete") { param -> .override("onFetchConversationWithMessagesComplete") { param ->
onSuccess(param.arg<List<*>>(1).map { Message(it) }) onSuccess(param.arg<List<*>>(1).map { Message(it) })
} }
@ -54,7 +64,7 @@ class ConversationManager(
fetchConversationWithMessagesMethod.invoke( fetchConversationWithMessagesMethod.invoke(
instanceNonNull(), instanceNonNull(),
conversationId.toSnapUUID().instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(),
CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")) CallbackBuilder(getCallbackClass("FetchConversationWithMessagesCallback"))
.override("onFetchConversationWithMessagesComplete") { param -> .override("onFetchConversationWithMessagesComplete") { param ->
onSuccess(param.arg<List<*>>(1).map { Message(it) }) onSuccess(param.arg<List<*>>(1).map { Message(it) })
} }
@ -70,7 +80,7 @@ class ConversationManager(
instanceNonNull(), instanceNonNull(),
conversationId.toSnapUUID().instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(),
messageId, messageId,
CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) CallbackBuilder(getCallbackClass("Callback"))
.override("onSuccess") { onResult(null) } .override("onSuccess") { onResult(null) }
.override("onError") { onResult(it.arg<Any>(0).toString()) }.build() .override("onError") { onResult(it.arg<Any>(0).toString()) }.build()
) )
@ -81,7 +91,7 @@ class ConversationManager(
instanceNonNull(), instanceNonNull(),
conversationId.toSnapUUID().instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(),
messageId, messageId,
CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback")) CallbackBuilder(getCallbackClass("FetchMessageCallback"))
.override("onFetchMessageComplete") { param -> .override("onFetchMessageComplete") { param ->
onSuccess(Message(param.arg(0))) onSuccess(Message(param.arg(0)))
} }
@ -100,7 +110,7 @@ class ConversationManager(
fetchMessageByServerId.invoke( fetchMessageByServerId.invoke(
instanceNonNull(), instanceNonNull(),
serverMessageIdentifier, serverMessageIdentifier,
CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback")) CallbackBuilder(getCallbackClass("FetchMessageCallback"))
.override("onFetchMessageComplete") { param -> .override("onFetchMessageComplete") { param ->
onSuccess(Message(param.arg(0))) onSuccess(Message(param.arg(0)))
} }
@ -119,7 +129,7 @@ class ConversationManager(
setObjectField("mServerMessageId", it) setObjectField("mServerMessageId", it)
} }
}, },
CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessagesByServerIdsCallback")) CallbackBuilder(getCallbackClass("FetchMessagesByServerIdsCallback"))
.override("onSuccess") { param -> .override("onSuccess") { param ->
onSuccess(param.arg<List<*>>(0).mapNotNull { onSuccess(param.arg<List<*>>(0).mapNotNull {
Message(it?.getObjectField("mMessage") ?: return@mapNotNull null) Message(it?.getObjectField("mMessage") ?: return@mapNotNull null)
@ -132,14 +142,14 @@ class ConversationManager(
} }
fun clearConversation(conversationId: String, onSuccess: () -> Unit, onError: (error: String) -> Unit) { fun clearConversation(conversationId: String, onSuccess: () -> Unit, onError: (error: String) -> Unit) {
val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) val callback = CallbackBuilder(getCallbackClass("Callback"))
.override("onSuccess") { onSuccess() } .override("onSuccess") { onSuccess() }
.override("onError") { onError(it.arg<Any>(0).toString()) }.build() .override("onError") { onError(it.arg<Any>(0).toString()) }.build()
clearConversation.invoke(instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), callback) clearConversation.invoke(instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), callback)
} }
fun getOneOnOneConversationIds(userIds: List<String>, onSuccess: (List<Pair<String, String>>) -> Unit, onError: (error: String) -> Unit) { fun getOneOnOneConversationIds(userIds: List<String>, onSuccess: (List<Pair<String, String>>) -> Unit, onError: (error: String) -> Unit) {
val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "GetOneOnOneConversationIdsCallback")) val callback = CallbackBuilder(getCallbackClass("GetOneOnOneConversationIdsCallback"))
.override("onSuccess") { param -> .override("onSuccess") { param ->
onSuccess(param.arg<ArrayList<*>>(0).map { onSuccess(param.arg<ArrayList<*>>(0).map {
SnapUUID(it.getObjectField("mUserId")).toString() to SnapUUID(it.getObjectField("mConversationId")).toString() SnapUUID(it.getObjectField("mUserId")).toString() to SnapUUID(it.getObjectField("mConversationId")).toString()

View File

@ -1,8 +1,103 @@
package me.rhunk.snapenhance.mapper package me.rhunk.snapenhance.mapper
abstract class AbstractClassMapper { import android.util.Log
import com.google.gson.Gson
import com.google.gson.JsonObject
abstract class AbstractClassMapper(
val mapperName: String
) {
lateinit var classLoader: ClassLoader
private val gson = Gson()
private val values = mutableMapOf<String, Any?>()
private val mappers = mutableListOf<MapperContext.() -> Unit>() private val mappers = mutableListOf<MapperContext.() -> Unit>()
private fun findClassSafe(className: String?) = runCatching {
classLoader.loadClass(className)
}.onFailure {
Log.e("Mapper", it.stackTraceToString())
}.getOrNull()
@Suppress("UNCHECKED_CAST")
inner class PropertyDelegate<T>(
private val key: String,
defaultValue: Any? = null,
private val setter: (Any?) -> Unit = { values[key] = it },
private val getter: (Any?) -> T? = { it as? T }
) {
init {
values[key] = defaultValue
}
operator fun getValue(thisRef: Any?, property: Any?): T? {
return getter(values[key])
}
operator fun setValue(thisRef: Any?, property: Any?, value: Any?) {
setter(value)
}
fun set(value: String?) {
values[key] = value
}
fun get(): T? {
return getter(values[key])
}
fun getAsClass(): Class<*>? {
return getter(values[key]) as? Class<*>
}
fun getAsString(): String? {
return getter(values[key])?.toString()
}
fun getClass(key: String): Class<*>? {
return (get() as? Map<String, String?>)?.let {
findClassSafe(it[key].toString())
}
}
override fun toString() = getter(values[key]).toString()
}
fun string(key: String): PropertyDelegate<String> = PropertyDelegate(key, null)
fun classReference(key: String): PropertyDelegate<Class<*>> = PropertyDelegate(key, getter = { findClassSafe(it as? String) })
fun map(key: String, value: MutableMap<String, String?> = mutableMapOf()): PropertyDelegate<MutableMap<String, String?>> = PropertyDelegate(key, value)
fun readFromJson(json: JsonObject) {
values.forEach { (key, _) ->
runCatching {
val jsonElement = json.get(key) ?: return@forEach
when (jsonElement) {
is JsonObject -> values[key] = gson.fromJson(jsonElement, HashMap::class.java)
else -> values[key] = jsonElement.asString
}
}.onFailure {
Log.e("Mapper","Failed to deserialize property $key")
}
}
}
fun writeFromJson(json: JsonObject) {
values.forEach { (key, value) ->
runCatching {
when (value) {
is String -> json.addProperty(key, value)
is Class<*> -> json.addProperty(key, value.name)
is Map<*, *> -> json.add(key, gson.toJsonTree(value))
else -> json.addProperty(key, value.toString())
}
}.onFailure {
Log.e("Mapper","Failed to serialize property $key")
}
}
}
fun mapper(task: MapperContext.() -> Unit) { fun mapper(task: MapperContext.() -> Unit) {
mappers.add(task) mappers.add(task)
} }

View File

@ -3,8 +3,8 @@ package me.rhunk.snapenhance.mapper
import com.google.gson.JsonObject import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.mapper.impl.*
import org.jf.dexlib2.Opcodes import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.dexbacked.DexBackedDexFile import org.jf.dexlib2.dexbacked.DexBackedDexFile
import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.ClassDef
@ -12,12 +12,33 @@ import java.io.BufferedInputStream
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipFile import java.util.zip.ZipFile
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import kotlin.reflect.KClass
class Mapper( class ClassMapper(
private vararg val mappers: KClass<out AbstractClassMapper> = arrayOf() private vararg val mappers: AbstractClassMapper = DEFAULT_MAPPERS,
) { ) {
private val classes = mutableListOf<ClassDef>() private val classes = mutableListOf<ClassDef>()
companion object {
val DEFAULT_MAPPERS get() = arrayOf(
BCryptClassMapper(),
CallbackMapper(),
DefaultMediaItemMapper(),
MediaQualityLevelProviderMapper(),
OperaPageViewControllerMapper(),
PlusSubscriptionMapper(),
ScCameraSettingsMapper(),
StoryBoostStateMapper(),
FriendsFeedEventDispatcherMapper(),
CompositeConfigurationProviderMapper(),
ScoreUpdateMapper(),
FriendRelationshipChangerMapper(),
ViewBinderMapper(),
FriendingDataSourcesMapper(),
OperaViewerParamsMapper(),
)
}
fun loadApk(path: String) { fun loadApk(path: String) {
val apkFile = ZipFile(path) val apkFile = ZipFile(path)
val apkEntries = apkFile.entries().toList() val apkEntries = apkFile.entries().toList()
@ -50,20 +71,23 @@ class Mapper(
} }
} }
fun start(): JsonObject { suspend fun run(): JsonObject {
val mappers = mappers.map { it.java.constructors.first().newInstance() as AbstractClassMapper }
val context = MapperContext(classes.associateBy { it.type }) val context = MapperContext(classes.associateBy { it.type })
runBlocking { withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { mappers.forEach { mapper ->
mappers.forEach { mapper -> launch {
launch { mapper.run(context)
mapper.run(context)
}
} }
} }
} }
return context.exportToJson() val outputJson = JsonObject()
mappers.forEach { mapper ->
outputJson.add(mapper.mapperName, JsonObject().apply {
mapper.writeFromJson(this)
})
}
return outputJson
} }
} }

View File

@ -1,7 +1,5 @@
package me.rhunk.snapenhance.mapper package me.rhunk.snapenhance.mapper
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.ClassDef
class MapperContext( class MapperContext(
@ -19,40 +17,4 @@ class MapperContext(
if (name == null) return null if (name == null) return null
return classMap[name.toString()] return classMap[name.toString()]
} }
private val mappings = mutableMapOf<String, Any?>()
fun addMapping(key: String, vararg array: Pair<String, Any?>) {
mappings[key] = array.toMap()
}
fun addMapping(key: String, value: String) {
mappings[key] = value
}
fun getStringMapping(key: String): String? {
return mappings[key] as? String
}
fun getMapMapping(key: String): Map<*, *>? {
return mappings[key] as? Map<*, *>
}
fun exportToJson(): JsonObject {
val gson = GsonBuilder().setPrettyPrinting().create()
val json = JsonObject()
for ((key, value) in mappings) {
when (value) {
is String -> json.addProperty(key, value)
is Map<*, *> -> {
val obj = JsonObject()
for ((k, v) in value) {
obj.add(k.toString(), gson.toJsonTree(v))
}
json.add(key, obj)
}
}
}
return json
}
} }

View File

@ -6,7 +6,10 @@ import me.rhunk.snapenhance.mapper.ext.getStaticConstructor
import me.rhunk.snapenhance.mapper.ext.isFinal import me.rhunk.snapenhance.mapper.ext.isFinal
import org.jf.dexlib2.iface.instruction.formats.ArrayPayload import org.jf.dexlib2.iface.instruction.formats.ArrayPayload
class BCryptClassMapper : AbstractClassMapper() { class BCryptClassMapper : AbstractClassMapper("BCryptClass") {
val classReference = classReference("class")
val hashMethod = string("hashMethod")
init { init {
mapper { mapper {
for (clazz in classes) { for (clazz in classes) {
@ -17,17 +20,15 @@ class BCryptClassMapper : AbstractClassMapper() {
} }
if (isBcryptClass == true) { if (isBcryptClass == true) {
val hashMethod = clazz.methods.first { val hashDexMethod = clazz.methods.first {
it.parameterTypes.size == 2 && it.parameterTypes.size == 2 &&
it.parameterTypes[0] == "Ljava/lang/String;" && it.parameterTypes[0] == "Ljava/lang/String;" &&
it.parameterTypes[1] == "Ljava/lang/String;" && it.parameterTypes[1] == "Ljava/lang/String;" &&
it.returnType == "Ljava/lang/String;" it.returnType == "Ljava/lang/String;"
} }
addMapping("BCrypt", hashMethod.set(hashDexMethod.name)
"class" to clazz.getClassName(), classReference.set(clazz.getClassName())
"hashMethod" to hashMethod.name
)
return@mapper return@mapper
} }
} }

View File

@ -4,11 +4,12 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
import me.rhunk.snapenhance.mapper.ext.getSuperClassName import me.rhunk.snapenhance.mapper.ext.getSuperClassName
import me.rhunk.snapenhance.mapper.ext.isFinal import me.rhunk.snapenhance.mapper.ext.isFinal
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.instruction.formats.Instruction21t import org.jf.dexlib2.iface.instruction.formats.Instruction21t
import org.jf.dexlib2.iface.instruction.formats.Instruction22t import org.jf.dexlib2.iface.instruction.formats.Instruction22t
class CallbackMapper : AbstractClassMapper() { class CallbackMapper : AbstractClassMapper("Callbacks") {
val callbacks = map("callbacks")
init { init {
mapper { mapper {
val callbackClasses = classes.filter { clazz -> val callbackClasses = classes.filter { clazz ->
@ -32,7 +33,7 @@ class CallbackMapper : AbstractClassMapper() {
it.getSuperClassName()!!.substringAfterLast("/") to it.getClassName() it.getSuperClassName()!!.substringAfterLast("/") to it.getClassName()
} }
addMapping("callbacks", *callbackClasses.toTypedArray()) callbacks.get()?.putAll(callbackClasses)
} }
} }
} }

View File

@ -1,14 +1,32 @@
package me.rhunk.snapenhance.mapper.impl package me.rhunk.snapenhance.mapper.impl
import me.rhunk.snapenhance.mapper.AbstractClassMapper import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.* import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName
import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString
import me.rhunk.snapenhance.mapper.ext.isEnum
import org.jf.dexlib2.iface.instruction.formats.Instruction21c import org.jf.dexlib2.iface.instruction.formats.Instruction21c
import org.jf.dexlib2.iface.instruction.formats.Instruction35c import org.jf.dexlib2.iface.instruction.formats.Instruction35c
import org.jf.dexlib2.iface.reference.FieldReference import org.jf.dexlib2.iface.reference.FieldReference
import org.jf.dexlib2.iface.reference.MethodReference import org.jf.dexlib2.iface.reference.MethodReference
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
class CompositeConfigurationProviderMapper : AbstractClassMapper() { class CompositeConfigurationProviderMapper : AbstractClassMapper("CompositeConfigurationProvider") {
val classReference = classReference("class")
val observeProperty = string("observeProperty")
val getProperty = string("getProperty")
val configEnumMapping = mapOf(
"class" to classReference("enumClass"),
"getValue" to string("enumGetValue"),
"getCategory" to string("enumGetCategory"),
"defaultValueField" to string("enumDefaultValueField")
)
val appExperimentProvider = mapOf(
"class" to classReference("appExperimentProviderClass"),
"getBooleanAppExperimentClass" to classReference("getBooleanAppExperimentClass"),
"hasExperimentMethod" to string("hasExperimentMethod")
)
init { init {
mapper { mapper {
for (classDef in classes) { for (classDef in classes) {
@ -68,24 +86,21 @@ class CompositeConfigurationProviderMapper : AbstractClassMapper() {
it.type == "Ljava/lang/Object;" it.type == "Ljava/lang/Object;"
} }
addMapping("CompositeConfigurationProvider", classReference.set(classDef.getClassName())
"class" to classDef.getClassName(), observeProperty.set(observePropertyMethod.name)
"observeProperty" to observePropertyMethod.name, getProperty.set(getPropertyMethod.name)
"getProperty" to getPropertyMethod.name,
"enum" to mapOf( configEnumMapping["class"]?.set(configEnumInterface.getClassName())
"class" to configEnumInterface.getClassName(), configEnumMapping["getValue"]?.set(enumGetDefaultValueMethod.name)
"getValue" to enumGetDefaultValueMethod.name, configEnumMapping["getCategory"]?.set(enumGetCategoryMethod.name)
"getCategory" to enumGetCategoryMethod.name, configEnumMapping["defaultValueField"]?.set(defaultValueField.name)
"defaultValueField" to defaultValueField.name
), hasExperimentMethodReference?.let {
"appExperimentProvider" to (hasExperimentMethodReference?.let { appExperimentProvider["class"]?.set(getClass(it.definingClass)?.getClassName())
mapOf( appExperimentProvider["getBooleanAppExperimentClass"]?.set(getBooleanAppExperimentClass)
"class" to getClass(it.definingClass)?.getClassName(), appExperimentProvider["hasExperimentMethod"]?.set(hasExperimentMethodReference.name)
"GetBooleanAppExperimentClass" to getBooleanAppExperimentClass, }
"hasExperimentMethod" to hasExperimentMethodReference.name
)
})
)
return@mapper return@mapper
} }
} }

View File

@ -5,16 +5,21 @@ import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isAbstract
class DefaultMediaItemMapper : AbstractClassMapper() { class DefaultMediaItemMapper : AbstractClassMapper("DefaultMediaItem") {
val cameraRollMediaId = classReference("cameraRollMediaIdClass")
val durationMsField = string("durationMsField")
val defaultMediaItem = classReference("defaultMediaItemClass")
init { init {
mapper { mapper {
for (clazz in classes) { for (clazz in classes) {
if (clazz.methods.find { it.name == "toString" }?.implementation?.findConstString("CameraRollMediaId", contains = true) != true) { if (clazz.methods.find { it.name == "toString" }?.implementation?.findConstString("CameraRollMediaId", contains = true) != true) {
continue continue
} }
val durationMsField = clazz.fields.firstOrNull { it.type == "J" } ?: continue val durationMsDexField = clazz.fields.firstOrNull { it.type == "J" } ?: continue
addMapping("CameraRollMediaId", "class" to clazz.getClassName(), "durationMsField" to durationMsField.name) cameraRollMediaId.set(clazz.getClassName())
durationMsField.set(durationMsDexField.name)
return@mapper return@mapper
} }
} }
@ -29,7 +34,7 @@ class DefaultMediaItemMapper : AbstractClassMapper() {
val constructorParameters = clazz.directMethods.firstOrNull { it.name == "<init>" }?.parameterTypes ?: continue val constructorParameters = clazz.directMethods.firstOrNull { it.name == "<init>" }?.parameterTypes ?: continue
if (constructorParameters.size < 6 || constructorParameters[5] != "J") continue if (constructorParameters.size < 6 || constructorParameters[5] != "J") continue
addMapping("DefaultMediaItem", clazz.getClassName()) defaultMediaItem.set(clazz.getClassName())
return@mapper return@mapper
} }
} }

View File

@ -5,12 +5,16 @@ import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
import me.rhunk.snapenhance.mapper.ext.isEnum import me.rhunk.snapenhance.mapper.ext.isEnum
class FriendRelationshipChangerMapper : AbstractClassMapper() { class FriendRelationshipChangerMapper : AbstractClassMapper("FriendRelationshipChanger") {
val classReference = classReference("class")
val addFriendMethod = string("addFriendMethod")
val removeFriendMethod = string("removeFriendMethod")
init { init {
mapper { mapper {
for (classDef in classes) { for (classDef in classes) {
classDef.methods.firstOrNull { it.name == "<init>" }?.implementation?.findConstString("FriendRelationshipChangerImpl")?.takeIf { it } ?: continue classDef.methods.firstOrNull { it.name == "<init>" }?.implementation?.findConstString("FriendRelationshipChangerImpl")?.takeIf { it } ?: continue
val addFriendMethod = classDef.methods.first { val addFriendDexMethod = classDef.methods.first {
it.parameterTypes.size > 4 && it.parameterTypes.size > 4 &&
getClass(it.parameterTypes[1])?.isEnum() == true && getClass(it.parameterTypes[1])?.isEnum() == true &&
getClass(it.parameterTypes[2])?.isEnum() == true && getClass(it.parameterTypes[2])?.isEnum() == true &&
@ -18,7 +22,7 @@ class FriendRelationshipChangerMapper : AbstractClassMapper() {
it.parameters[4].type == "Ljava/lang/String;" it.parameters[4].type == "Ljava/lang/String;"
} }
val removeFriendMethod = classDef.methods.first { val removeFriendDexMethod = classDef.methods.firstOrNull {
it.parameterTypes.size == 5 && it.parameterTypes.size == 5 &&
it.parameterTypes[0] == "Ljava/lang/String;" && it.parameterTypes[0] == "Ljava/lang/String;" &&
getClass(it.parameterTypes[1])?.isEnum() == true && getClass(it.parameterTypes[1])?.isEnum() == true &&
@ -26,11 +30,12 @@ class FriendRelationshipChangerMapper : AbstractClassMapper() {
it.parameterTypes[3] == "Ljava/lang/String;" it.parameterTypes[3] == "Ljava/lang/String;"
} }
addMapping("FriendRelationshipChanger", this@FriendRelationshipChangerMapper.apply {
"class" to classDef.getClassName(), classReference.set(classDef.getClassName())
"addFriendMethod" to addFriendMethod.name, addFriendMethod.set(addFriendDexMethod.name)
"removeFriendMethod" to removeFriendMethod.name removeFriendMethod.set(removeFriendDexMethod?.name)
) }
return@mapper return@mapper
} }
} }

View File

@ -5,7 +5,10 @@ import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
import me.rhunk.snapenhance.mapper.ext.searchNextFieldReference import me.rhunk.snapenhance.mapper.ext.searchNextFieldReference
class FriendingDataSourcesMapper: AbstractClassMapper() { class FriendingDataSourcesMapper: AbstractClassMapper("FriendingDataSources") {
val classReference = classReference("class")
val quickAddSourceListField = string("quickAddSourceListField")
init { init {
mapper { mapper {
for (classDef in classes) { for (classDef in classes) {
@ -15,13 +18,11 @@ class FriendingDataSourcesMapper: AbstractClassMapper() {
val toStringMethod = classDef.methods.firstOrNull { it.name == "toString" } ?: continue val toStringMethod = classDef.methods.firstOrNull { it.name == "toString" } ?: continue
if (toStringMethod.implementation?.findConstString("quickaddSource", contains = true) != true) continue if (toStringMethod.implementation?.findConstString("quickaddSource", contains = true) != true) continue
val quickAddSourceListField = toStringMethod.implementation?.searchNextFieldReference("quickaddSource", contains = true) val quickAddSourceListDexField = toStringMethod.implementation?.searchNextFieldReference("quickaddSource", contains = true)
?: continue ?: continue
addMapping("FriendingDataSources", classReference.set(classDef.getClassName())
"class" to classDef.getClassName(), quickAddSourceListField.set(quickAddSourceListDexField.name)
"quickAddSourceListField" to quickAddSourceListField.name
)
return@mapper return@mapper
} }
} }

View File

@ -5,7 +5,10 @@ import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
class FriendsFeedEventDispatcherMapper : AbstractClassMapper() { class FriendsFeedEventDispatcherMapper : AbstractClassMapper("FriendsFeedEventDispatcher") {
val classReference = classReference("class")
val viewModelField = string("viewModelField")
init { init {
mapper { mapper {
for (clazz in classes) { for (clazz in classes) {
@ -13,15 +16,13 @@ class FriendsFeedEventDispatcherMapper : AbstractClassMapper() {
val onItemLongPress = clazz.methods.first { it.name == "onItemLongPress" } val onItemLongPress = clazz.methods.first { it.name == "onItemLongPress" }
val viewHolderContainerClass = getClass(onItemLongPress.parameterTypes[0]) ?: continue val viewHolderContainerClass = getClass(onItemLongPress.parameterTypes[0]) ?: continue
val viewModelField = viewHolderContainerClass.fields.firstOrNull { field -> val viewModelDexField = viewHolderContainerClass.fields.firstOrNull { field ->
val typeClass = getClass(field.type) ?: return@firstOrNull false val typeClass = getClass(field.type) ?: return@firstOrNull false
typeClass.methods.firstOrNull {it.name == "toString"}?.implementation?.findConstString("FriendFeedItemViewModel", contains = true) == true typeClass.methods.firstOrNull {it.name == "toString"}?.implementation?.findConstString("FriendFeedItemViewModel", contains = true) == true
}?.name ?: continue }?.name ?: continue
addMapping("FriendsFeedEventDispatcher", classReference.set(clazz.getClassName())
"class" to clazz.getClassName(), viewModelField.set(viewModelDexField)
"viewModelField" to viewModelField
)
return@mapper return@mapper
} }
} }

View File

@ -7,7 +7,10 @@ import me.rhunk.snapenhance.mapper.ext.isAbstract
import me.rhunk.snapenhance.mapper.ext.isEnum import me.rhunk.snapenhance.mapper.ext.isEnum
import org.jf.dexlib2.AccessFlags import org.jf.dexlib2.AccessFlags
class MediaQualityLevelProviderMapper : AbstractClassMapper() { class MediaQualityLevelProviderMapper : AbstractClassMapper("MediaQualityLevelProvider") {
val mediaQualityLevelProvider = classReference("mediaQualityLevelProvider")
val mediaQualityLevelProviderMethod = string("mediaQualityLevelProviderMethod")
init { init {
var enumQualityLevel : String? = null var enumQualityLevel : String? = null
@ -20,7 +23,6 @@ class MediaQualityLevelProviderMapper : AbstractClassMapper() {
break; break;
} }
} }
addMapping("EnumQualityLevel", enumQualityLevel ?: return@mapper)
} }
mapper { mapper {
@ -31,10 +33,8 @@ class MediaQualityLevelProviderMapper : AbstractClassMapper() {
if (clazz.fields.none { it.accessFlags and AccessFlags.TRANSIENT.value != 0 }) continue if (clazz.fields.none { it.accessFlags and AccessFlags.TRANSIENT.value != 0 }) continue
clazz.methods.firstOrNull { it.returnType == "L$enumQualityLevel;" }?.let { clazz.methods.firstOrNull { it.returnType == "L$enumQualityLevel;" }?.let {
addMapping("MediaQualityLevelProvider", mediaQualityLevelProvider.set(clazz.getClassName())
"class" to clazz.getClassName(), mediaQualityLevelProviderMethod.set(it.name)
"method" to it.name
)
return@mapper return@mapper
} }
} }

View File

@ -7,7 +7,13 @@ import me.rhunk.snapenhance.mapper.ext.hasStaticConstructorString
import me.rhunk.snapenhance.mapper.ext.isAbstract import me.rhunk.snapenhance.mapper.ext.isAbstract
import me.rhunk.snapenhance.mapper.ext.isEnum import me.rhunk.snapenhance.mapper.ext.isEnum
class OperaPageViewControllerMapper : AbstractClassMapper() { class OperaPageViewControllerMapper : AbstractClassMapper("OperaPageViewController") {
val classReference = classReference("class")
val viewStateField = string("viewStateField")
val layerListField = string("layerListField")
val onDisplayStateChange = string("onDisplayStateChange")
val onDisplayStateChangeGesture = string("onDisplayStateChangeGesture")
init { init {
mapper { mapper {
for (clazz in classes) { for (clazz in classes) {
@ -16,37 +22,35 @@ class OperaPageViewControllerMapper : AbstractClassMapper() {
continue continue
} }
val viewStateField = clazz.fields.first { field -> val viewStateDexField = clazz.fields.first { field ->
val fieldClass = getClass(field.type) ?: return@first false val fieldClass = getClass(field.type) ?: return@first false
fieldClass.isEnum() && fieldClass.hasStaticConstructorString("FULLY_DISPLAYED") fieldClass.isEnum() && fieldClass.hasStaticConstructorString("FULLY_DISPLAYED")
} }
val layerListField = clazz.fields.first { it.type == "Ljava/util/ArrayList;" } val layerListDexField = clazz.fields.first { it.type == "Ljava/util/ArrayList;" }
val onDisplayStateChange = clazz.methods.firstOrNull { val onDisplayStateChangeDexMethod = clazz.methods.firstOrNull {
if (it.returnType != "V" || it.parameterTypes.size != 1) return@firstOrNull false if (it.returnType != "V" || it.parameterTypes.size != 1) return@firstOrNull false
val firstParameterType = getClass(it.parameterTypes[0]) ?: return@firstOrNull false val firstParameterType = getClass(it.parameterTypes[0]) ?: return@firstOrNull false
if (firstParameterType.type == clazz.type || !firstParameterType.isAbstract()) return@firstOrNull false if (firstParameterType.type == clazz.type || !firstParameterType.isAbstract()) return@firstOrNull false
//check if the class contains a field with the enumViewStateClass type //check if the class contains a field with the enumViewStateClass type
firstParameterType.fields.any { field -> firstParameterType.fields.any { field ->
field.type == viewStateField.type field.type == viewStateDexField.type
} }
} }
val onDisplayStateChangeGesture = clazz.methods.first { val onDisplayStateChangeGestureDexMethod = clazz.methods.first {
if (it.returnType != "V" || it.parameterTypes.size != 2) return@first false if (it.returnType != "V" || it.parameterTypes.size != 2) return@first false
val firstParameterType = getClass(it.parameterTypes[0]) ?: return@first false val firstParameterType = getClass(it.parameterTypes[0]) ?: return@first false
val secondParameterType = getClass(it.parameterTypes[1]) ?: return@first false val secondParameterType = getClass(it.parameterTypes[1]) ?: return@first false
firstParameterType.isEnum() && secondParameterType.isEnum() firstParameterType.isEnum() && secondParameterType.isEnum()
} }
addMapping("OperaPageViewController", classReference.set(clazz.getClassName())
"class" to clazz.getClassName(), viewStateField.set(viewStateDexField.name)
"viewStateField" to viewStateField.name, layerListField.set(layerListDexField.name)
"layerListField" to layerListField.name, onDisplayStateChange.set(onDisplayStateChangeDexMethod?.name)
"onDisplayStateChange" to onDisplayStateChange?.name, onDisplayStateChangeGesture.set(onDisplayStateChangeGestureDexMethod.name)
"onDisplayStateChangeGesture" to onDisplayStateChangeGesture.name
)
return@mapper return@mapper
} }

View File

@ -6,14 +6,17 @@ import me.rhunk.snapenhance.mapper.ext.getClassName
import org.jf.dexlib2.iface.instruction.formats.Instruction35c import org.jf.dexlib2.iface.instruction.formats.Instruction35c
import org.jf.dexlib2.iface.reference.MethodReference import org.jf.dexlib2.iface.reference.MethodReference
class OperaViewerParamsMapper : AbstractClassMapper() { class OperaViewerParamsMapper : AbstractClassMapper("OperaViewerParams") {
val classReference = classReference("class")
val putMethod = string("putMethod")
init { init {
mapper { mapper {
for (classDef in classes) { for (classDef in classes) {
classDef.fields.firstOrNull { it.type == "Ljava/util/concurrent/ConcurrentHashMap;" } ?: continue classDef.fields.firstOrNull { it.type == "Ljava/util/concurrent/ConcurrentHashMap;" } ?: continue
if (classDef.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("Params") != true) continue if (classDef.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("Params") != true) continue
val putMethod = classDef.methods.firstOrNull { method -> val putDexMethod = classDef.methods.firstOrNull { method ->
method.implementation?.instructions?.any { method.implementation?.instructions?.any {
val instruction = it as? Instruction35c ?: return@any false val instruction = it as? Instruction35c ?: return@any false
val reference = instruction.reference as? MethodReference ?: return@any false val reference = instruction.reference as? MethodReference ?: return@any false
@ -21,10 +24,9 @@ class OperaViewerParamsMapper : AbstractClassMapper() {
} == true } == true
} ?: return@mapper } ?: return@mapper
addMapping("OperaViewerParams", classReference.set(classDef.getClassName())
"class" to classDef.getClassName(), putMethod.set(putDexMethod.name)
"putMethod" to putMethod.name
)
return@mapper return@mapper
} }
} }

View File

@ -4,24 +4,26 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
class PlusSubscriptionMapper : AbstractClassMapper(){ class PlusSubscriptionMapper : AbstractClassMapper("PlusSubscription"){
val classReference = classReference("class")
init { init {
mapper { mapper {
for (clazz in classes) { for (clazz in classes) {
if (clazz.directMethods.filter { it.name == "<init>" }.none { if (clazz.directMethods.filter { it.name == "<init>" }.none {
it.parameters.size == 4 && it.parameterTypes.size > 3 &&
it.parameterTypes[0] == "I" && it.parameterTypes[0] == "I" &&
it.parameterTypes[1] == "I" && it.parameterTypes[1] == "I" &&
it.parameterTypes[2] == "J" && it.parameterTypes[2] == "J" &&
it.parameterTypes[3] == "J" it.parameterTypes[3] == "J"
}) continue }) continue
val isPlusSubscriptionInfoClass = clazz.virtualMethods.firstOrNull { it.name == "toString" }?.implementation?.let { val isPlusSubscriptionInfoClass = clazz.virtualMethods.firstOrNull { it.name == "toString" }?.implementation?.let {
it.findConstString("SubscriptionInfo", contains = true) && it.findConstString("expirationTimeMillis", contains = true) it.findConstString("SubscriptionInfo", contains = true) && it.findConstString("expirationTimeMillis", contains = true)
} }
if (isPlusSubscriptionInfoClass == true) { if (isPlusSubscriptionInfoClass == true) {
addMapping("SubscriptionInfoClass", clazz.getClassName()) classReference.set(clazz.getClassName())
return@mapper return@mapper
} }
} }

View File

@ -6,7 +6,9 @@ import me.rhunk.snapenhance.mapper.ext.getClassName
import me.rhunk.snapenhance.mapper.ext.getStaticConstructor import me.rhunk.snapenhance.mapper.ext.getStaticConstructor
import me.rhunk.snapenhance.mapper.ext.isEnum import me.rhunk.snapenhance.mapper.ext.isEnum
class ScCameraSettingsMapper : AbstractClassMapper() { class ScCameraSettingsMapper : AbstractClassMapper("ScCameraSettings") {
val classReference = classReference("class")
init { init {
mapper { mapper {
for (clazz in classes) { for (clazz in classes) {
@ -15,7 +17,7 @@ class ScCameraSettingsMapper : AbstractClassMapper() {
val firstParameter = getClass(firstConstructor.parameterTypes[0]) ?: continue val firstParameter = getClass(firstConstructor.parameterTypes[0]) ?: continue
if (!firstParameter.isEnum() || firstParameter.getStaticConstructor()?.implementation?.findConstString("CONTINUOUS_PICTURE") != true) continue if (!firstParameter.isEnum() || firstParameter.getStaticConstructor()?.implementation?.findConstString("CONTINUOUS_PICTURE") != true) continue
addMapping("ScCameraSettings", clazz.getClassName()) classReference.set(clazz.getClassName())
return@mapper return@mapper
} }
} }

View File

@ -4,7 +4,9 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
class ScoreUpdateMapper : AbstractClassMapper() { class ScoreUpdateMapper : AbstractClassMapper("ScoreUpdate") {
val classReference = classReference("class")
init { init {
mapper { mapper {
for (classDef in classes) { for (classDef in classes) {
@ -18,7 +20,7 @@ class ScoreUpdateMapper : AbstractClassMapper() {
it.name == "toString" it.name == "toString"
}?.implementation?.findConstString("Friend.sq:selectFriendUserScoresNeedToUpdate") != true) continue }?.implementation?.findConstString("Friend.sq:selectFriendUserScoresNeedToUpdate") != true) continue
addMapping("ScoreUpdate", classDef.getClassName()) classReference.set(classDef.getClassName())
return@mapper return@mapper
} }
} }

View File

@ -4,7 +4,9 @@ import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.findConstString import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName import me.rhunk.snapenhance.mapper.ext.getClassName
class StoryBoostStateMapper : AbstractClassMapper() { class StoryBoostStateMapper : AbstractClassMapper("StoryBoostState") {
val classReference = classReference("class")
init { init {
mapper { mapper {
for (clazz in classes) { for (clazz in classes) {
@ -14,7 +16,7 @@ class StoryBoostStateMapper : AbstractClassMapper() {
if (clazz.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("StoryBoostState", contains = true) != true) continue if (clazz.methods.firstOrNull { it.name == "toString" }?.implementation?.findConstString("StoryBoostState", contains = true) != true) continue
addMapping("StoryBoostStateClass", clazz.getClassName()) classReference.set(clazz.getClassName())
return@mapper return@mapper
} }
} }

View File

@ -6,13 +6,17 @@ import me.rhunk.snapenhance.mapper.ext.isAbstract
import me.rhunk.snapenhance.mapper.ext.isInterface import me.rhunk.snapenhance.mapper.ext.isInterface
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
class ViewBinderMapper : AbstractClassMapper() { class ViewBinderMapper : AbstractClassMapper("ViewBinder") {
val classReference = classReference("class")
val bindMethod = string("bindMethod")
val getViewMethod = string("getViewMethod")
init { init {
mapper { mapper {
for (clazz in classes) { for (clazz in classes) {
if (!clazz.isAbstract() || clazz.isInterface()) continue if (!clazz.isAbstract() || clazz.isInterface()) continue
val getViewMethod = clazz.methods.firstOrNull { it.returnType == "Landroid/view/View;" && it.parameterTypes.size == 0 } ?: continue val getViewDexMethod = clazz.methods.firstOrNull { it.returnType == "Landroid/view/View;" && it.parameterTypes.size == 0 } ?: continue
// update view // update view
clazz.methods.filter { clazz.methods.filter {
@ -21,17 +25,15 @@ class ViewBinderMapper : AbstractClassMapper() {
if (it.size != 1) return@also if (it.size != 1) return@also
}.firstOrNull() ?: continue }.firstOrNull() ?: continue
val bindMethod = clazz.methods.filter { val bindDexMethod = clazz.methods.filter {
Modifier.isAbstract(it.accessFlags) && it.parameterTypes.size == 2 && it.parameterTypes[0] == it.parameterTypes[1] && it.returnType == "V" Modifier.isAbstract(it.accessFlags) && it.parameterTypes.size == 2 && it.parameterTypes[0] == it.parameterTypes[1] && it.returnType == "V"
}.also { }.also {
if (it.size != 1) return@also if (it.size != 1) return@also
}.firstOrNull() ?: continue }.firstOrNull() ?: continue
addMapping("ViewBinder", classReference.set(clazz.getClassName())
"class" to clazz.getClassName(), bindMethod.set(bindDexMethod.name)
"bindMethod" to bindMethod.name, getViewMethod.set(getViewDexMethod.name)
"getViewMethod" to getViewMethod.name
)
return@mapper return@mapper
} }
} }

View File

@ -1,7 +1,8 @@
package me.rhunk.snapenhance.mapper.tests package me.rhunk.snapenhance.mapper.tests
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import me.rhunk.snapenhance.mapper.Mapper import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.mapper.ClassMapper
import me.rhunk.snapenhance.mapper.impl.* import me.rhunk.snapenhance.mapper.impl.*
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
@ -10,28 +11,14 @@ import java.io.File
class TestMappings { class TestMappings {
@Test @Test
fun testMappings() { fun testMappings() {
val mapper = Mapper( val classMapper = ClassMapper()
BCryptClassMapper::class,
CallbackMapper::class,
DefaultMediaItemMapper::class,
MediaQualityLevelProviderMapper::class,
OperaPageViewControllerMapper::class,
PlusSubscriptionMapper::class,
ScCameraSettingsMapper::class,
StoryBoostStateMapper::class,
FriendsFeedEventDispatcherMapper::class,
CompositeConfigurationProviderMapper::class,
ScoreUpdateMapper::class,
FriendRelationshipChangerMapper::class,
ViewBinderMapper::class,
FriendingDataSourcesMapper::class,
OperaViewerParamsMapper::class,
)
val gson = GsonBuilder().setPrettyPrinting().create() val gson = GsonBuilder().setPrettyPrinting().create()
val apkFile = File(System.getenv("SNAPCHAT_APK")!!) val apkFile = File(System.getenv("SNAPCHAT_APK")!!)
mapper.loadApk(apkFile.absolutePath) classMapper.loadApk(apkFile.absolutePath)
val result = mapper.start() runBlocking {
println("Mappings: ${gson.toJson(result)}") val result = classMapper.run()
println("Mappings: ${gson.toJson(result)}")
}
} }
} }