feat(experimental): custom streaks expiration format

This commit is contained in:
rhunk
2024-04-18 00:59:17 +02:00
parent 68c61a0b73
commit c44c36dbd4
6 changed files with 97 additions and 0 deletions

View File

@@ -898,6 +898,10 @@
"name": "Hidden Snapchat Plus Features",
"description": "Enables unreleased/beta Snapchat Plus features\nMight not work on older Snapchat versions"
},
"custom_streaks_expiration_format": {
"name": "Custom Streaks Expiration Format",
"description": "Customizes the Streaks Expiration format\n\nAvailable variables:\n - %c: Streaks Count\n - %e: Hourglass Emoji\n - %d: Days\n - %h: Hours\n - %m: Minutes\n - %s: Seconds\n - %w: Remaining Time"
},
"disable_composer_modules": {
"name": "Disable Composer Modules",
"description": "Prevents selected composer modules from being loaded\nNames must be separated by a comma"

View File

@@ -45,6 +45,7 @@ class Experimental : ConfigContainer() {
addNotices(FeatureNotice.BAN_RISK, FeatureNotice.UNSTABLE)
requireRestart()
}
val customStreaksExpirationFormat = string("custom_streaks_expiration_format") { requireRestart() }
val addFriendSourceSpoof = unique("add_friend_source_spoof",
"added_by_username",
"added_by_mention",

View File

@@ -129,6 +129,7 @@ class FeatureManager(
MediaFilePicker(),
HideActiveMusic(),
AutoOpenSnaps(),
CustomStreaksExpirationFormat(),
)
initializeFeatures()

View File

@@ -0,0 +1,43 @@
package me.rhunk.snapenhance.core.features.impl.ui
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.mapper.impl.StreaksExpirationMapper
import kotlin.time.Duration.Companion.milliseconds
class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
private fun Long.padZero(): String {
return this.toString().padStart(2, '0')
}
override fun onActivityCreate() {
val expirationFormat by context.config.experimental.customStreaksExpirationFormat
if (expirationFormat.isEmpty()) return
context.mappings.useMapper(StreaksExpirationMapper::class) {
findClass(streaksFormatterClass.get() ?: return@useMapper).hook(formatStreaksTextMethod.get() ?: return@useMapper, HookStage.AFTER) { param ->
val streaksCount = param.argNullable(2) ?: 0
val streaksExpiration = param.argNullable<Any>(3) ?: return@hook
val hourGlassTimeRemaining = streaksExpiration.getObjectField(hourGlassTimeRemainingField.get() ?: return@hook) as? Long ?: return@hook
val expirationTime = streaksExpiration.getObjectField(expirationTimeField.get() ?: return@hook) as? Long ?: return@hook
val delta = (expirationTime - System.currentTimeMillis()).milliseconds
val hourGlassEmoji = if (delta.inWholeMilliseconds in 1..hourGlassTimeRemaining) if (expirationTime % 2 == 0L) "" else "" else ""
param.setResult(expirationFormat
.replace("%c", streaksCount.toString())
.replace("%e", hourGlassEmoji)
.replace("%d", delta.inWholeDays.toString())
.replace("%h", (delta.inWholeHours % 24).padZero())
.replace("%m", (delta.inWholeMinutes % 60).padZero())
.replace("%s", (delta.inWholeSeconds % 60).padZero())
.replace("%w", delta.toString())
)
}
}
}
}

View File

@@ -35,6 +35,7 @@ class ClassMapper(
FriendingDataSourcesMapper(),
OperaViewerParamsMapper(),
MemoriesPresenterMapper(),
StreaksExpirationMapper(),
)
}

View File

@@ -0,0 +1,47 @@
package me.rhunk.snapenhance.mapper.impl
import me.rhunk.snapenhance.mapper.AbstractClassMapper
import me.rhunk.snapenhance.mapper.ext.findConstString
import me.rhunk.snapenhance.mapper.ext.getClassName
import me.rhunk.snapenhance.mapper.ext.searchNextFieldReference
import java.lang.reflect.Modifier
class StreaksExpirationMapper: AbstractClassMapper("StreaksExpirationMapper") {
val hourGlassTimeRemainingField = string("hourGlassTimeRemainingField")
val expirationTimeField = string("expirationTimeField")
val streaksFormatterClass = string("streaksFormatterClass")
val formatStreaksTextMethod = string("formatStreaksTextMethod")
init {
mapper {
var streaksExpirationClassName: String? = null
for (clazz in classes) {
val toStringMethod = clazz.methods.firstOrNull { it.name == "toString" } ?: continue
if (toStringMethod.implementation?.findConstString("StreaksExpiration(", contains = true) != true) continue
streaksExpirationClassName = clazz.getClassName()
toStringMethod.implementation?.apply {
hourGlassTimeRemainingField.set(searchNextFieldReference("hourGlassTimeRemaining", contains = true)?.name)
expirationTimeField.set(searchNextFieldReference("expirationTime", contains = true)?.name)
}
break
}
if (streaksExpirationClassName == null) return@mapper
for (clazz in classes) {
val formatStreaksTextDexMethod = clazz.methods.firstOrNull { method ->
Modifier.isStatic(method.accessFlags) &&
method.returnType == "Ljava/lang/String;" &&
method.parameterTypes.let {
it.size >= 4 && it[0] == "Ljava/util/Map;" && it[2] == "Ljava/lang/Integer;" && it[3].contains(streaksExpirationClassName)
}
} ?: continue
streaksFormatterClass.set(clazz.getClassName())
formatStreaksTextMethod.set(formatStreaksTextDexMethod.name)
break
}
}
}
}