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:
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(