commit 0ba1eb4a8b067c8c8535409b3d432c912dc2e1a3
parent e4443279d67e61d6f858324a6d745cfb3608bd04
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun,  8 Oct 2023 18:41:18 +0200

feat: bypass video length restriction
- single and split mode

Diffstat:
Mcore/src/main/assets/lang/en_US.json | 10+++++++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt | 2+-
Acore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/BypassVideoLengthRestriction.kt | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt | 51---------------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt | 2+-
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt | 13+++++++++++++
6 files changed, 95 insertions(+), 56 deletions(-)

diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -362,9 +362,9 @@ "name": "Block Ads", "description": "Prevents Advertisements from being displayed" }, - "disable_video_length_restrictions": { - "name": "Disable Video Length Restrictions", - "description": "Disables Snapchat's maximum video length restriction" + "bypass_video_length_restriction": { + "name": "Bypass Video Length Restrictions", + "description": "Single: sends a single video\nSplit: split videos after editing" }, "disable_google_play_dialogs": { "name": "Disable Google Play Services Dialogs", @@ -664,6 +664,10 @@ "added_by_group_chat": "By Group Chat", "added_by_qr_code": "By QR Code", "added_by_community": "By Community" + }, + "bypass_video_length_restriction": { + "single": "Single media", + "split": "Split media" } } }, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt @@ -7,7 +7,7 @@ class Global : ConfigContainer() { val snapchatPlus = boolean("snapchat_plus") { addNotices(FeatureNotice.BAN_RISK); requireRestart() } val disableMetrics = boolean("disable_metrics") val blockAds = boolean("block_ads") - val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions") { addNotices(FeatureNotice.BAN_RISK) } + val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices(FeatureNotice.BAN_RISK); requireRestart() } val disableGooglePlayDialogs = boolean("disable_google_play_dialogs") { requireRestart() } val forceMediaSourceQuality = boolean("force_media_source_quality") val disableSnapSplitting = boolean("disable_snap_splitting") { addNotices(FeatureNotice.INTERNAL_BEHAVIOR) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/BypassVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/BypassVideoLengthRestriction.kt @@ -0,0 +1,72 @@ +package me.rhunk.snapenhance.features.impl.tweaks + +import android.os.Build +import android.os.FileObserver +import com.google.gson.JsonParser +import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent +import me.rhunk.snapenhance.core.util.ktx.setObjectField +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.hookConstructor +import java.io.File + +class BypassVideoLengthRestriction : + Feature("BypassVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + private lateinit var fileObserver: FileObserver + + override fun asyncOnActivityCreate() { + val mode = context.config.global.bypassVideoLengthRestriction.getNullable() + + if (mode == "single") { + //fix black videos when story is posted + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val postedStorySnapFolder = + File(context.androidContext.filesDir, "file_manager/posted_story_snap") + + fileObserver = (object : FileObserver(postedStorySnapFolder, MOVED_TO) { + override fun onEvent(event: Int, path: String?) { + if (event != MOVED_TO || path?.endsWith("posted_story_snap.2") != true) return + fileObserver.stopWatching() + + val file = File(postedStorySnapFolder, path) + runCatching { + val fileContent = JsonParser.parseReader(file.reader()).asJsonObject + if (fileContent["timerOrDuration"].asLong < 0) file.delete() + }.onFailure { + context.log.error("Failed to read story metadata file", it) + } + } + }) + + context.event.subscribe(SendMessageWithContentEvent::class) { event -> + if (event.destinations.stories.isEmpty()) return@subscribe + fileObserver.startWatching() + } + } + + context.mappings.getMappedClass("DefaultMediaItem") + .hookConstructor(HookStage.BEFORE) { param -> + //set the video length argument + param.setArg(5, -1L) + } + } + + //TODO: allow split from any source + if (mode == "split") { + val cameraRollId = context.mappings.getMappedMap("CameraRollMediaId") + // memories grid + findClass(cameraRollId["class"].toString()).hookConstructor(HookStage.AFTER) { param -> + //set the durationMs field + param.thisObject<Any>() + .setObjectField(cameraRollId["durationMsField"].toString(), -1L) + } + + // chat camera roll grid + findClass("com.snap.impala.common.media.MediaLibraryItem").hookConstructor(HookStage.BEFORE) { param -> + //set the video length argument + param.setArg(3, -1L) + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt @@ -1,50 +0,0 @@ -package me.rhunk.snapenhance.features.impl.tweaks - -import android.os.Build -import android.os.FileObserver -import com.google.gson.JsonParser -import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent -import me.rhunk.snapenhance.features.Feature -import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker -import java.io.File - -class DisableVideoLengthRestriction : Feature("DisableVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - private lateinit var fileObserver: FileObserver - - override fun asyncOnActivityCreate() { - val defaultMediaItem = context.mappings.getMappedClass("DefaultMediaItem") - val isState by context.config.global.disableVideoLengthRestrictions - - //fix black videos when story is posted - if (isState && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val postedStorySnapFolder = File(context.androidContext.filesDir, "file_manager/posted_story_snap") - - fileObserver = (object : FileObserver(postedStorySnapFolder, MOVED_TO) { - override fun onEvent(event: Int, path: String?) { - if (event != MOVED_TO || path?.endsWith("posted_story_snap.2") != true) return - fileObserver.stopWatching() - - val file = File(postedStorySnapFolder, path) - runCatching { - val fileContent = JsonParser.parseReader(file.reader()).asJsonObject - if (fileContent["timerOrDuration"].asLong < 0) file.delete() - }.onFailure { - context.log.error("Failed to read story metadata file", it) - } - } - }) - - context.event.subscribe(SendMessageWithContentEvent::class) { event -> - if (event.destinations.stories.isEmpty()) return@subscribe - fileObserver.startWatching() - } - } - - Hooker.hookConstructor(defaultMediaItem, HookStage.BEFORE, { isState }) { param -> - //set the video length argument - param.setArg(5, -1L) - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -84,7 +84,7 @@ class FeatureManager( ConfigurationOverride::class, SendOverride::class, UnlimitedSnapViewTime::class, - DisableVideoLengthRestriction::class, + BypassVideoLengthRestriction::class, MediaQualityLevelOverride::class, MeoPasscodeBypass::class, AppPasscode::class, diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/DefaultMediaItemMapper.kt @@ -1,6 +1,7 @@ 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.isAbstract @@ -8,6 +9,18 @@ class DefaultMediaItemMapper : AbstractClassMapper() { init { mapper { for (clazz in classes) { + if (clazz.methods.find { it.name == "toString" }?.implementation?.findConstString("CameraRollMediaId", contains = true) != true) { + continue + } + val durationMsField = clazz.fields.firstOrNull { it.type == "J" } ?: continue + + addMapping("CameraRollMediaId", "class" to clazz.getClassName(), "durationMsField" to durationMsField.name) + return@mapper + } + } + + mapper { + for (clazz in classes) { val superClass = getClass(clazz.superclass) ?: continue if (!superClass.isAbstract() || superClass.interfaces.isEmpty() || superClass.interfaces[0] != "Ljava/lang/Comparable;") continue