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",
"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": {
"name": "Prevent Message Sending",
"description": "Prevents sending certain types of messages"
@ -982,6 +986,10 @@
"name": "Bypass Camera Roll Limit",
"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": {
"name": "Composer Console",
"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) {
val showFirstCreatedUsername = boolean("show_first_created_username")
val bypassCameraRollLimit = boolean("bypass_camera_roll_limit")
val customSelfDestructSnapDelay = boolean("custom_self_destruct_snap_delay")
val composerConsole = boolean("composer_console")
val composerLogs = boolean("composer_logs")
}

View File

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

View File

@ -1,4 +1,6 @@
export const jsx = require('composer_core/src/JSX').jsx;
export const assetCatalog = require("composer_core/src/AssetCatalog")
export const style = require("composer_core/src/Style");
import { runtimeName } from "./imports"
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");

View File

@ -1,7 +1,10 @@
import { Config, FriendInfo } from "./types";
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 {
return remoteImports[method](...args);

View File

@ -4,6 +4,7 @@ import { modules } from "./types";
import "./modules/operaDownloadButton";
import "./modules/firstCreatedUsername";
import "./modules/bypassCameraRollSelectionLimit";
import "./modules/selfDestructSnapDelay";
try {
@ -21,12 +22,13 @@ try {
}
try {
m.init();
console.debug(`module ${m.name} initialized`);
} catch (e) {
console.error(`failed to initialize module ${m.name}`, e, e.stack);
}
});
console.log("modules loaded!");
console.debug("modules loaded!");
} catch (e) {
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 showFirstCreatedUsername: boolean
readonly composerLogs: boolean
readonly customSelfDestructSnapDelay: boolean
}
export interface FriendInfo {

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,8 +199,7 @@ class ComposerHooks: Feature("ComposerHooks") {
}
}
findClass("com.snapchat.client.composer.NativeBridge").apply {
hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
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 ->
@ -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.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,35 +16,57 @@ 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)
}
}.onFailure {
context.log.warn("Failed to hook simpleStreaksFormatterClass : " + it.message)
}
}
}
if (expirationFormat.isEmpty()) return@onNextActivityCreate
simpleStreaksFormatterClass.getAsClass()?.hook(
formatSimpleStreaksTextMethod.get() ?: return@useMapper,
HookStage.AFTER
) { param ->
val result = param.getResult() as? String ?: return@hook
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
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 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 ""
val hourGlassEmoji =
if (delta.inWholeMilliseconds in 1..(15.hours.inWholeMilliseconds)) if (expirationTime % 2 == 0L) "\u23F3" else "\u231B" else ""
param.setResult(expirationFormat
.replace("%c", streaksCount.toString())
param.setResult(
expirationFormat
.replace("%c", streakCount.toString())
.replace("%e", hourGlassEmoji)
.replace("%d", delta.inWholeDays.toString())
.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) {
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) }

View File

@ -3,16 +3,9 @@ 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 = classReference("streaksFormatterClass")
val formatStreaksTextMethod = string("formatStreaksTextMethod")
val simpleStreaksFormatterClass = classReference("simpleStreaksFormatterClass")
val formatSimpleStreaksTextMethod = string("formatSimpleStreaksTextMethod")
@ -41,35 +34,5 @@ class StreaksExpirationMapper: AbstractClassMapper("StreaksExpirationMapper") {
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"