Merge branch 'dev' into dev_gh

This commit is contained in:
rhunk
2025-06-01 15:04:49 +02:00
15 changed files with 163 additions and 85 deletions

View File

@ -143,6 +143,7 @@ class FeatureManager(
FriendNotes(),
DoubleTapChatAction(),
SnapScoreChanges(),
DisableSnapModeRestrictions(),
)
features.values.toList().forEach { feature ->

View File

@ -104,6 +104,8 @@ class ComposerHooks: Feature("ComposerHooks") {
override fun init() {
if (config.globalState != true) return
val nativeBridgeClass = runCatching { findClass("com.snapchat.client.valdi.NativeBridge") }.getOrNull() ?: findClass("com.snapchat.client.composer.NativeBridge")
val importedFunctions = mutableMapOf<String, Any?>()
fun composerFunction(name: String, block: ComposerMarshaller.() -> Unit) {
@ -118,7 +120,8 @@ class ComposerHooks: Feature("ComposerHooks") {
"operaDownloadButton" to context.config.downloader.operaDownloadButton.get(),
"bypassCameraRollLimit" to config.bypassCameraRollLimit.get(),
"showFirstCreatedUsername" to config.showFirstCreatedUsername.get(),
"composerLogs" to config.composerLogs.get()
"composerLogs" to config.composerLogs.get(),
"customSelfDestructSnapDelay" to config.customSelfDestructSnapDelay.get(),
))
}
@ -172,7 +175,8 @@ class ComposerHooks: Feature("ComposerHooks") {
context.native.setComposerLoader("""
const i = setInterval(() => {
try {
require('composer_core/src/DeviceBridge').getDisplayWidth();
const _runtimeName = "${if (nativeBridgeClass.name == "com.snapchat.client.valdi.NativeBridge") "valdi" else "composer"}";
require(_runtimeName + '_core/src/DeviceBridge').getDisplayWidth();
clearInterval(i);
(() => { const _getImportsFunctionName = "$getImportsFunctionName"; $loaderScript })();
} catch (e) {}
@ -195,16 +199,14 @@ class ComposerHooks: Feature("ComposerHooks") {
}
}
findClass("com.snapchat.client.composer.NativeBridge").apply {
hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
val moduleFactory = param.argNullable<Any>(1) ?: return@hook
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString()?.contains("DeviceBridge") != true) return@hook
Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam ->
val result = methodParam.getResult() as? MutableMap<String, Any?> ?: return@ephemeralHookObjectMethod
result[getImportsFunctionName] = newComposerFunction {
pushUntyped(importedFunctions)
true
}
nativeBridgeClass.hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
val moduleFactory = param.argNullable<Any>(1) ?: return@hook
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString()?.contains("DeviceBridge") != true) return@hook
Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam ->
val result = methodParam.getResult() as? MutableMap<String, Any?> ?: return@ephemeralHookObjectMethod
result[getImportsFunctionName] = newComposerFunction {
pushUntyped(importedFunctions)
true
}
}
}

View File

@ -0,0 +1,18 @@
package me.rhunk.snapenhance.core.features.impl.tweaks
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.util.dataBuilder
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hookConstructor
class DisableSnapModeRestrictions: Feature("Disable Snap Mode Restrictions") {
override fun init() {
if (!context.config.messaging.disableSnapModeRestrictions.get()) return
findClass("com.snapchat.client.messaging.SnapModeInfo").hookConstructor(HookStage.AFTER) { param ->
param.thisObject<Any>().dataBuilder {
set("mSelfDestructSnapDurationMs", null)
}
}
}
}

View File

@ -3,8 +3,12 @@ package me.rhunk.snapenhance.core.features.impl.ui
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.mapper.impl.StreaksExpirationMapper
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat") {
@ -12,44 +16,68 @@ class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat") {
return this.toString().padStart(2, '0')
}
private val streakCache = ConcurrentHashMap<String, Pair<Int, Long>>()
override fun init() {
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
val conversationId = SnapUUID(param.thisObject<Any>().getObjectField("mConversationId")).toString()
val streakMetadata = param.thisObject<Any>().getObjectField("mStreakMetadata") ?: apply {
if (streakCache.containsKey(conversationId)) streakCache.remove(conversationId)
return@hookConstructor
}
streakCache.put(conversationId, streakMetadata.getObjectField("mCount") as Int to
streakMetadata.getObjectField("mExpirationTimestampMs") as Long)
}
onNextActivityCreate {
val expirationFormat by context.config.experimental.customStreaksExpirationFormat
if (expirationFormat.isNotEmpty() || context.config.userInterface.streakExpirationInfo.get()) {
context.mappings.useMapper(StreaksExpirationMapper::class) {
runCatching {
simpleStreaksFormatterClass.getAsClass()?.hook(formatSimpleStreaksTextMethod.get() ?: return@useMapper, HookStage.BEFORE) { param ->
param.setResult(null)
simpleStreaksFormatterClass.getAsClass()?.hook(
formatSimpleStreaksTextMethod.get() ?: return@useMapper,
HookStage.AFTER
) { param ->
val result = param.getResult() as? String ?: return@hook
streakCache[param.arg<String>(1)]?.also { (streakCount, expirationTime) ->
if (expirationTime <= 0L) return@also
if (expirationFormat.isEmpty()) {
val remainingTime = (expirationTime - System.currentTimeMillis()).milliseconds.inWholeHours
var emojiIndex = result.indexOfFirst { it.code > 127 }.takeIf { it != -1 }
?.let { it + 2 }
if (emojiIndex == null) {
emojiIndex = result.length
}
param.setResult(
result.substring(0, emojiIndex) + remainingTime + result.substring(emojiIndex)
)
return@hook
}
val delta = (expirationTime - System.currentTimeMillis()).milliseconds
val hourGlassEmoji =
if (delta.inWholeMilliseconds in 1..(15.hours.inWholeMilliseconds)) if (expirationTime % 2 == 0L) "\u23F3" else "\u231B" else ""
param.setResult(
expirationFormat
.replace("%c", streakCount.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())
)
}
}.onFailure {
context.log.warn("Failed to hook simpleStreaksFormatterClass : " + it.message)
}
}
}
if (expirationFormat.isEmpty()) return@onNextActivityCreate
context.mappings.useMapper(StreaksExpirationMapper::class) {
streaksFormatterClass.getAsClass()?.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) "\u23F3" else "\u231B" 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

@ -38,8 +38,7 @@ class MenuViewInjector : Feature("MenuViewInjector") {
onNextActivityCreate(defer = true) {
menuMap.forEach { it.value.init() }
val chatActionMenu = context.resources.getIdentifier("chat_action_menu", "id")
val hasV2ActionMenu = { context.feature(COFOverride::class).hasActionMenuV2 }
val hasV2ActionMenu = { true }
context.event.subscribe(AddViewEvent::class) { event ->
menuMap.forEach { it.value.onViewAdded(event) }