Merge branch 'dev' into dev_gh

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

View File

@ -638,6 +638,10 @@
"name": "Unlimited Conversation Pinning", "name": "Unlimited Conversation Pinning",
"description": "Allows you to pin an unlimited amount of conversations locally" "description": "Allows you to pin an unlimited amount of conversations locally"
}, },
"disable_snap_mode_restrictions": {
"name": "Disable Snap Mode Restrictions",
"description": "Allows you to view self-destructing Snaps without restrictions"
},
"prevent_message_sending": { "prevent_message_sending": {
"name": "Prevent Message Sending", "name": "Prevent Message Sending",
"description": "Prevents sending certain types of messages" "description": "Prevents sending certain types of messages"
@ -982,6 +986,10 @@
"name": "Bypass Camera Roll Limit", "name": "Bypass Camera Roll Limit",
"description": "Increases the maximum amount of media you can send from the camera roll" "description": "Increases the maximum amount of media you can send from the camera roll"
}, },
"custom_self_destruct_snap_delay": {
"name": "Custom Self Destruct Snap Delay",
"description": "Gives more options for the self-destruct timer when sending a Snap"
},
"composer_console": { "composer_console": {
"name": "Composer Console", "name": "Composer Console",
"description": "Allows you to execute JavaScript code in Composer (arm64 only)" "description": "Allows you to execute JavaScript code in Composer (arm64 only)"

View File

@ -27,6 +27,7 @@ class Experimental : ConfigContainer() {
class ComposerHooksConfig: ConfigContainer(hasGlobalState = true) { class ComposerHooksConfig: ConfigContainer(hasGlobalState = true) {
val showFirstCreatedUsername = boolean("show_first_created_username") val showFirstCreatedUsername = boolean("show_first_created_username")
val bypassCameraRollLimit = boolean("bypass_camera_roll_limit") val bypassCameraRollLimit = boolean("bypass_camera_roll_limit")
val customSelfDestructSnapDelay = boolean("custom_self_destruct_snap_delay")
val composerConsole = boolean("composer_console") val composerConsole = boolean("composer_console")
val composerLogs = boolean("composer_logs") val composerLogs = boolean("composer_logs")
} }

View File

@ -70,6 +70,7 @@ class MessagingTweaks : ConfigContainer() {
val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()} val halfSwipeNotifier = container("half_swipe_notifier", HalfSwipeNotifierConfig()) { requireRestart()}
val callStartConfirmation = boolean("call_start_confirmation") { requireRestart() } val callStartConfirmation = boolean("call_start_confirmation") { requireRestart() }
val unlimitedConversationPinning = boolean("unlimited_conversation_pinning") { requireRestart() } val unlimitedConversationPinning = boolean("unlimited_conversation_pinning") { requireRestart() }
val disableSnapModeRestrictions = boolean("disable_snap_mode_restrictions") { requireRestart() }
val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations", val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations",
"CHAT", "CHAT",
"SNAP", "SNAP",

View File

@ -1,4 +1,6 @@
export const jsx = require('composer_core/src/JSX').jsx; import { runtimeName } from "./imports"
export const assetCatalog = require("composer_core/src/AssetCatalog")
export const style = require("composer_core/src/Style"); export const jsx = require(runtimeName + '_core/src/JSX').jsx;
export const assetCatalog = require(runtimeName + "_core/src/AssetCatalog")
export const style = require(runtimeName + "_core/src/Style");
export const colors = require("coreui/src/styles/semanticColors"); export const colors = require("coreui/src/styles/semanticColors");

View File

@ -1,7 +1,10 @@
import { Config, FriendInfo } from "./types"; import { Config, FriendInfo } from "./types";
declare var _getImportsFunctionName: string; declare var _getImportsFunctionName: string;
const remoteImports = require('composer_core/src/DeviceBridge')[_getImportsFunctionName](); declare var _runtimeName: boolean;
export const runtimeName = _runtimeName;
const remoteImports = require(_runtimeName + '_core/src/DeviceBridge')[_getImportsFunctionName]();
function callRemoteFunction(method: string, ...args: any[]): any | null { function callRemoteFunction(method: string, ...args: any[]): any | null {
return remoteImports[method](...args); return remoteImports[method](...args);

View File

@ -4,6 +4,7 @@ import { modules } from "./types";
import "./modules/operaDownloadButton"; import "./modules/operaDownloadButton";
import "./modules/firstCreatedUsername"; import "./modules/firstCreatedUsername";
import "./modules/bypassCameraRollSelectionLimit"; import "./modules/bypassCameraRollSelectionLimit";
import "./modules/selfDestructSnapDelay";
try { try {
@ -21,12 +22,13 @@ try {
} }
try { try {
m.init(); m.init();
console.debug(`module ${m.name} initialized`);
} catch (e) { } catch (e) {
console.error(`failed to initialize module ${m.name}`, e, e.stack); console.error(`failed to initialize module ${m.name}`, e, e.stack);
} }
}); });
console.log("modules loaded!"); console.debug("modules loaded!");
} catch (e) { } catch (e) {
log("error", "Failed to load composer modules\n" + e + "\n" + e.stack) log("error", "Failed to load composer modules\n" + e + "\n" + e.stack)
} }

View File

@ -0,0 +1,47 @@
import { defineModule } from "../types";
import { interceptComponent } from "../utils";
export default defineModule({
name: "Self Destruct Snap Delay",
enabled: config => config.customSelfDestructSnapDelay,
init() {
interceptComponent(
'snap_editor_timer_tool/src/TimerPickerView',
'TimerPickerView',
{
"<init>": (args: any[], superCall: () => void) => {
if (args[1].options[0] == 30) {
args[1].style = 0; // seconds format
args[1].options = [
5, // 5 seconds
10, // 10 seconds
20, // 20 seconds
30, // 30 seconds
60, // 1 minute
120, // 2 minutes
180, // 3 minutes
240, // 4 minutes
300, // 5 minutes
600, // 10 minutes
900, // 15 minutes
1200, // 20 minutes
1800, // 30 minutes
3600, // 1 hour
7200, // 2 hours
10800, // 3 hours
14400, // 4 hours
21600, // 6 hours
28800, // 8 hours
43200, // 12 hours
86400, // 1 day
172800, // 2 days
]
}
superCall();
}
}
)
}
});

View File

@ -3,6 +3,7 @@ export interface Config {
readonly bypassCameraRollLimit: boolean readonly bypassCameraRollLimit: boolean
readonly showFirstCreatedUsername: boolean readonly showFirstCreatedUsername: boolean
readonly composerLogs: boolean readonly composerLogs: boolean
readonly customSelfDestructSnapDelay: boolean
} }
export interface FriendInfo { export interface FriendInfo {

View File

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

View File

@ -104,6 +104,8 @@ class ComposerHooks: Feature("ComposerHooks") {
override fun init() { override fun init() {
if (config.globalState != true) return 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?>() val importedFunctions = mutableMapOf<String, Any?>()
fun composerFunction(name: String, block: ComposerMarshaller.() -> Unit) { fun composerFunction(name: String, block: ComposerMarshaller.() -> Unit) {
@ -118,7 +120,8 @@ class ComposerHooks: Feature("ComposerHooks") {
"operaDownloadButton" to context.config.downloader.operaDownloadButton.get(), "operaDownloadButton" to context.config.downloader.operaDownloadButton.get(),
"bypassCameraRollLimit" to config.bypassCameraRollLimit.get(), "bypassCameraRollLimit" to config.bypassCameraRollLimit.get(),
"showFirstCreatedUsername" to config.showFirstCreatedUsername.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(""" context.native.setComposerLoader("""
const i = setInterval(() => { const i = setInterval(() => {
try { 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); clearInterval(i);
(() => { const _getImportsFunctionName = "$getImportsFunctionName"; $loaderScript })(); (() => { const _getImportsFunctionName = "$getImportsFunctionName"; $loaderScript })();
} catch (e) {} } catch (e) {}
@ -195,8 +199,7 @@ class ComposerHooks: Feature("ComposerHooks") {
} }
} }
findClass("com.snapchat.client.composer.NativeBridge").apply { nativeBridgeClass.hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
val moduleFactory = param.argNullable<Any>(1) ?: return@hook val moduleFactory = param.argNullable<Any>(1) ?: return@hook
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString()?.contains("DeviceBridge") != true) return@hook if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString()?.contains("DeviceBridge") != true) return@hook
Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam -> Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam ->
@ -208,5 +211,4 @@ class ComposerHooks: Feature("ComposerHooks") {
} }
} }
} }
}
} }

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.features.Feature
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.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.mapper.impl.StreaksExpirationMapper import me.rhunk.snapenhance.mapper.impl.StreaksExpirationMapper
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat") { class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat") {
@ -12,35 +16,57 @@ class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat") {
return this.toString().padStart(2, '0') return this.toString().padStart(2, '0')
} }
private val streakCache = ConcurrentHashMap<String, Pair<Int, Long>>()
override fun init() { 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 { onNextActivityCreate {
val expirationFormat by context.config.experimental.customStreaksExpirationFormat val expirationFormat by context.config.experimental.customStreaksExpirationFormat
if (expirationFormat.isNotEmpty() || context.config.userInterface.streakExpirationInfo.get()) { if (expirationFormat.isNotEmpty() || context.config.userInterface.streakExpirationInfo.get()) {
context.mappings.useMapper(StreaksExpirationMapper::class) { context.mappings.useMapper(StreaksExpirationMapper::class) {
runCatching { simpleStreaksFormatterClass.getAsClass()?.hook(
simpleStreaksFormatterClass.getAsClass()?.hook(formatSimpleStreaksTextMethod.get() ?: return@useMapper, HookStage.BEFORE) { param -> formatSimpleStreaksTextMethod.get() ?: return@useMapper,
param.setResult(null) HookStage.AFTER
} ) { param ->
}.onFailure { val result = param.getResult() as? String ?: return@hook
context.log.warn("Failed to hook simpleStreaksFormatterClass : " + it.message)
}
}
}
if (expirationFormat.isEmpty()) return@onNextActivityCreate
context.mappings.useMapper(StreaksExpirationMapper::class) { streakCache[param.arg<String>(1)]?.also { (streakCount, expirationTime) ->
streaksFormatterClass.getAsClass()?.hook(formatStreaksTextMethod.get() ?: return@useMapper, HookStage.AFTER) { param -> if (expirationTime <= 0L) return@also
val streaksCount = param.argNullable(2) ?: 0
val streaksExpiration = param.argNullable<Any>(3) ?: return@hook 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 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 delta = (expirationTime - System.currentTimeMillis()).milliseconds
val hourGlassEmoji = if (delta.inWholeMilliseconds in 1..hourGlassTimeRemaining) if (expirationTime % 2 == 0L) "\u23F3" else "\u231B" else "" val hourGlassEmoji =
if (delta.inWholeMilliseconds in 1..(15.hours.inWholeMilliseconds)) if (expirationTime % 2 == 0L) "\u23F3" else "\u231B" else ""
param.setResult(expirationFormat param.setResult(
.replace("%c", streaksCount.toString()) expirationFormat
.replace("%c", streakCount.toString())
.replace("%e", hourGlassEmoji) .replace("%e", hourGlassEmoji)
.replace("%d", delta.inWholeDays.toString()) .replace("%d", delta.inWholeDays.toString())
.replace("%h", (delta.inWholeHours % 24).padZero()) .replace("%h", (delta.inWholeHours % 24).padZero())
@ -52,4 +78,6 @@ class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat") {
} }
} }
} }
}
}
} }

View File

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

View File

@ -3,16 +3,9 @@ package me.rhunk.snapenhance.mapper.impl
import me.rhunk.snapenhance.mapper.AbstractClassMapper 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
import me.rhunk.snapenhance.mapper.ext.searchNextFieldReference
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
class StreaksExpirationMapper: AbstractClassMapper("StreaksExpirationMapper") { class StreaksExpirationMapper: AbstractClassMapper("StreaksExpirationMapper") {
val hourGlassTimeRemainingField = string("hourGlassTimeRemainingField")
val expirationTimeField = string("expirationTimeField")
val streaksFormatterClass = classReference("streaksFormatterClass")
val formatStreaksTextMethod = string("formatStreaksTextMethod")
val simpleStreaksFormatterClass = classReference("simpleStreaksFormatterClass") val simpleStreaksFormatterClass = classReference("simpleStreaksFormatterClass")
val formatSimpleStreaksTextMethod = string("formatSimpleStreaksTextMethod") val formatSimpleStreaksTextMethod = string("formatSimpleStreaksTextMethod")
@ -41,35 +34,5 @@ class StreaksExpirationMapper: AbstractClassMapper("StreaksExpirationMapper") {
return@mapper return@mapper
} }
} }
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
}
}
} }
} }

View File

@ -0,0 +1,2 @@
[build]
target = "aarch64-linux-android"