diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 24b1dcdc..044c7269 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -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)" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt index c00e18fd..fb5de705 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt @@ -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") } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt index eaa42727..6938b66f 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt @@ -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", diff --git a/composer/src/main/ts/composer.ts b/composer/src/main/ts/composer.ts index 49465b87..01ed38ed 100644 --- a/composer/src/main/ts/composer.ts +++ b/composer/src/main/ts/composer.ts @@ -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"); diff --git a/composer/src/main/ts/imports.ts b/composer/src/main/ts/imports.ts index f44ca511..ac49e612 100644 --- a/composer/src/main/ts/imports.ts +++ b/composer/src/main/ts/imports.ts @@ -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); diff --git a/composer/src/main/ts/main.ts b/composer/src/main/ts/main.ts index a53f2c26..afcb27e2 100644 --- a/composer/src/main/ts/main.ts +++ b/composer/src/main/ts/main.ts @@ -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) } diff --git a/composer/src/main/ts/modules/selfDestructSnapDelay.ts b/composer/src/main/ts/modules/selfDestructSnapDelay.ts new file mode 100644 index 00000000..68ded6ac --- /dev/null +++ b/composer/src/main/ts/modules/selfDestructSnapDelay.ts @@ -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', + { + "": (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(); + } + } + ) + } +}); \ No newline at end of file diff --git a/composer/src/main/ts/types.ts b/composer/src/main/ts/types.ts index 9c3189f6..e19a40c8 100644 --- a/composer/src/main/ts/types.ts +++ b/composer/src/main/ts/types.ts @@ -3,6 +3,7 @@ export interface Config { readonly bypassCameraRollLimit: boolean readonly showFirstCreatedUsername: boolean readonly composerLogs: boolean + readonly customSelfDestructSnapDelay: boolean } export interface FriendInfo { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt index ef755ac4..43a728c4 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt @@ -143,6 +143,7 @@ class FeatureManager( FriendNotes(), DoubleTapChatAction(), SnapScoreChanges(), + DisableSnapModeRestrictions(), ) features.values.toList().forEach { feature -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt index fbdc37d0..badd56f0 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt @@ -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() 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(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 ?: return@ephemeralHookObjectMethod - result[getImportsFunctionName] = newComposerFunction { - pushUntyped(importedFunctions) - true - } + nativeBridgeClass.hook("registerNativeModuleFactory", HookStage.BEFORE) { param -> + val moduleFactory = param.argNullable(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 ?: return@ephemeralHookObjectMethod + result[getImportsFunctionName] = newComposerFunction { + pushUntyped(importedFunctions) + true } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/DisableSnapModeRestrictions.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/DisableSnapModeRestrictions.kt new file mode 100644 index 00000000..ba44d2d3 --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/DisableSnapModeRestrictions.kt @@ -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().dataBuilder { + set("mSelfDestructSnapDurationMs", null) + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt index 8d9945f9..c2fec3be 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomStreaksExpirationFormat.kt @@ -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>() + override fun init() { + context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> + val conversationId = SnapUUID(param.thisObject().getObjectField("mConversationId")).toString() + + val streakMetadata = param.thisObject().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(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(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()) - ) - } - } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt index 57f60fca..e94b971a 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt @@ -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) } diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StreaksExpirationMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StreaksExpirationMapper.kt index e82778f5..0a63b383 100644 --- a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StreaksExpirationMapper.kt +++ b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/StreaksExpirationMapper.kt @@ -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 - } - } } } \ No newline at end of file diff --git a/native/rust/.cargo/config.toml b/native/rust/.cargo/config.toml new file mode 100644 index 00000000..351acab4 --- /dev/null +++ b/native/rust/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "aarch64-linux-android" \ No newline at end of file