commit e9b9a71a7ed2da68741a3017d2b668110559d23e
parent 7d4963770da449f259111cdd763769a06fe3fa77
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Wed, 22 Nov 2023 00:02:18 +0100

feat: story features
- disable rewatch indicator
- disable public stories

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 8++++++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt | 1+
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt | 2++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/NetworkApiRequestEvent.kt | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Acore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/Stories.kt | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AnonymousStoryViewing.kt | 27---------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/Hooker.kt | 7+++++--
9 files changed, 159 insertions(+), 32 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -324,6 +324,10 @@ "name": "Anonymous Story Viewing", "description": "Prevents anyone from knowing you've seen their story" }, + "prevent_story_rewatch_indicator": { + "name": "Prevent Story Rewatch Indicator", + "description": "Prevents anyone from knowing you've rewatched their story" + }, "hide_peek_a_peek": { "name": "Hide Peek-a-Peek", "description": "Prevents notification from being sent when you half swipe into a chat" @@ -420,6 +424,10 @@ "name": "Disable Metrics", "description": "Blocks sending specific analytic data to Snapchat" }, + "disable_public_stories": { + "name": "Disable Public Stories", + "description": "Removes every public story from the Discover page\nMay require a clean cache to work properly" + }, "block_ads": { "name": "Block Ads", "description": "Prevents Advertisements from being displayed" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Global.kt @@ -11,6 +11,7 @@ class Global : ConfigContainer() { val snapchatPlus = boolean("snapchat_plus") { requireRestart() } val disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() } val disableMetrics = boolean("disable_metrics") + val disablePublicStories = boolean("disable_public_stories") { requireRestart(); requireCleanCache() } val blockAds = boolean("block_ads") val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices( FeatureNotice.BAN_RISK); requireRestart(); nativeHooks() } 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 @@ -7,6 +7,7 @@ import me.rhunk.snapenhance.common.data.NotificationType class MessagingTweaks : ConfigContainer() { val bypassScreenshotDetection = boolean("bypass_screenshot_detection") { requireRestart() } val anonymousStoryViewing = boolean("anonymous_story_viewing") + val preventStoryRewatchIndicator = boolean("prevent_story_rewatch_indicator") { requireRestart() } val hidePeekAPeek = boolean("hide_peek_a_peek") val hideBitmojiPresence = boolean("hide_bitmoji_presence") val hideTypingNotifications = boolean("hide_typing_notifications") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt @@ -136,11 +136,13 @@ class EventDispatcher( NetworkApiRequestEvent( url = request.getObjectField("mUrl") as String, callback = param.arg(4), + uploadDataProvider = param.argNullable(5), request = request, ).apply { adapter = param } ) { + if (canceled) param.setResult(null) request.setObjectField("mUrl", url) postHookEvent() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/NetworkApiRequestEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/NetworkApiRequestEvent.kt @@ -1,9 +1,93 @@ package me.rhunk.snapenhance.core.event.events.impl import me.rhunk.snapenhance.core.event.events.AbstractHookEvent +import me.rhunk.snapenhance.core.util.hook.HookAdapter +import me.rhunk.snapenhance.core.util.hook.HookStage +import me.rhunk.snapenhance.core.util.hook.Hooker +import java.nio.ByteBuffer class NetworkApiRequestEvent( val request: Any, + val uploadDataProvider: Any?, val callback: Any, var url: String, -) : AbstractHookEvent()- \ No newline at end of file +) : AbstractHookEvent() { + fun addResultHook(methodName: String, stage: HookStage = HookStage.BEFORE, callback: (HookAdapter) -> Unit) { + Hooker.ephemeralHookObjectMethod( + this.callback::class.java, + this.callback, + methodName, + stage + ) { callback.invoke(it) } + } + + fun onSuccess(callback: HookAdapter.(ByteArray?) -> Unit) { + addResultHook("onSucceeded") { param -> + callback.invoke(param, param.argNullable<ByteBuffer>(2)?.let { + ByteArray(it.capacity()).also { buffer -> it.get(buffer); it.position(0) } + }) + } + } + + fun hookRequestBuffer(onRequest: (ByteArray) -> ByteArray) { + val streamDataProvider = this.uploadDataProvider?.let { provider -> + provider::class.java.methods.find { it.name == "getUploadStreamDataProvider" }?.invoke(provider) + } ?: return + val streamDataProviderMethods = streamDataProvider::class.java.methods + + val originalBufferSize = streamDataProviderMethods.find { it.name == "getLength" }?.invoke(streamDataProvider) as? Long ?: return + var originalRequestBuffer = ByteArray(originalBufferSize.toInt()) + streamDataProviderMethods.find { it.name == "read" }?.invoke(streamDataProvider, ByteBuffer.wrap(originalRequestBuffer)) + streamDataProviderMethods.find { it.name == "close" }?.invoke(streamDataProvider) + + runCatching { + originalRequestBuffer = onRequest.invoke(originalRequestBuffer) + }.onFailure { + context.log.error("Failed to hook request buffer", it) + } + + var offset = 0L + val unhooks = mutableListOf<() -> Unit>() + + fun hookObjectMethod(methodName: String, callback: (HookAdapter) -> Unit) { + Hooker.hookObjectMethod( + streamDataProvider::class.java, + streamDataProvider, + methodName, + HookStage.BEFORE + ) { + callback.invoke(it) + }.also { unhooks.addAll(it) } + } + + hookObjectMethod("getLength") { it.setResult(originalRequestBuffer.size.toLong()) } + hookObjectMethod("getOffset") { it.setResult(offset) } + hookObjectMethod("close") { param -> + unhooks.forEach { it.invoke() } + param.setResult(null) + } + hookObjectMethod("rewind") { + offset = 0 + it.setResult(true) + } + hookObjectMethod("read") { param -> + val byteBuffer = param.arg<ByteBuffer>(0) + val length = originalRequestBuffer.size.coerceAtMost(byteBuffer.remaining()) + byteBuffer.put(originalRequestBuffer, offset.toInt(), length) + offset += length + param.setResult(byteBuffer.position().toLong()) + } + + Hooker.hookObjectMethod( + this.uploadDataProvider::class.java, + this.uploadDataProvider, + "getUploadStreamDataProvider", + HookStage.BEFORE + ) { + if (it.nullableThisObject<Any>() != this.uploadDataProvider) return@hookObjectMethod + it.setResult(streamDataProvider) + }.also { + unhooks.addAll(it) + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/Stories.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/Stories.kt @@ -0,0 +1,53 @@ +package me.rhunk.snapenhance.core.features.impl + +import kotlinx.coroutines.runBlocking +import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor +import me.rhunk.snapenhance.common.util.protobuf.ProtoReader +import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import java.nio.ByteBuffer +import kotlin.coroutines.suspendCoroutine + +class Stories : Feature("Stories", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + override fun onActivityCreate() { + val disablePublicStories by context.config.global.disablePublicStories + + context.event.subscribe(NetworkApiRequestEvent::class) { event -> + fun cancelRequest() { + runBlocking { + suspendCoroutine { + context.httpServer.ensureServerStarted { + event.url = "http://127.0.0.1:${context.httpServer.port}" + it.resumeWith(Result.success(Unit)) + } + } + } + } + + if (event.url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) { + if (context.config.messaging.anonymousStoryViewing.get()) { + cancelRequest() + return@subscribe + } + if (!context.config.messaging.preventStoryRewatchIndicator.get()) return@subscribe + event.hookRequestBuffer { buffer -> + if (ProtoReader(buffer).getVarInt(2, 7, 4) == 1L) { + cancelRequest() + } + buffer + } + } + + if (disablePublicStories && (event.url.endsWith("df-mixer-prod/stories") || event.url.endsWith("df-mixer-prod/batch_stories"))) { + event.onSuccess { buffer -> + val payload = ProtoEditor(buffer ?: return@onSuccess).apply { + edit(3) { remove(3) } + }.toByteArray() + setArg(2, ByteBuffer.wrap(payload)) + } + return@subscribe + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AnonymousStoryViewing.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AnonymousStoryViewing.kt @@ -1,27 +0,0 @@ -package me.rhunk.snapenhance.core.features.impl.messaging - -import kotlinx.coroutines.runBlocking -import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent -import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams -import me.rhunk.snapenhance.core.util.media.HttpServer -import kotlin.coroutines.suspendCoroutine - -class AnonymousStoryViewing : Feature("Anonymous Story Viewing", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { - val anonymousStoryViewProperty by context.config.messaging.anonymousStoryViewing - val httpServer = HttpServer() - - context.event.subscribe(NetworkApiRequestEvent::class, { anonymousStoryViewProperty }) { event -> - if (!event.url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) return@subscribe - runBlocking { - suspendCoroutine { - httpServer.ensureServerStarted { - event.url = "http://127.0.0.1:${httpServer.port}" - it.resumeWith(Result.success(Unit)) - } - } - } - } - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -19,6 +19,7 @@ import me.rhunk.snapenhance.core.features.impl.spying.HalfSwipeNotifier import me.rhunk.snapenhance.core.features.impl.spying.StealthMode import me.rhunk.snapenhance.core.features.impl.tweaks.CameraTweaks import me.rhunk.snapenhance.core.features.impl.tweaks.BypassScreenshotDetection +import me.rhunk.snapenhance.core.features.impl.Stories import me.rhunk.snapenhance.core.features.impl.ui.* import me.rhunk.snapenhance.core.logger.CoreLogger import me.rhunk.snapenhance.core.manager.Manager @@ -68,7 +69,6 @@ class FeatureManager( StealthMode::class, MenuViewInjector::class, PreventReadReceipts::class, - AnonymousStoryViewing::class, MessageLogger::class, SnapchatPlus::class, DisableMetrics::class, @@ -108,6 +108,7 @@ class FeatureManager( BypassScreenshotDetection::class, HalfSwipeNotifier::class, DisableConfirmationDialogs::class, + Stories::class, ) initializeFeatures() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/Hooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/hook/Hooker.kt @@ -75,8 +75,8 @@ object Hooker { methodName: String, stage: HookStage, crossinline hookConsumer: (HookAdapter) -> Unit - ) { - val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet() + ): List<() -> Unit> { + val unhooks = mutableSetOf<XC_MethodHook.Unhook>() hook(clazz, methodName, stage) { param-> if (param.nullableThisObject<Any>().let { if (it == null) unhooks.forEach { u -> u.unhook() } @@ -84,6 +84,9 @@ object Hooker { }) return@hook hookConsumer(param) }.also { unhooks.addAll(it) } + return unhooks.map { + { it.unhook() } + } } inline fun ephemeralHook(