feat: story features

- disable rewatch indicator
- disable public stories
This commit is contained in:
rhunk 2023-11-22 00:02:18 +01:00
parent 7d4963770d
commit e9b9a71a7e
9 changed files with 157 additions and 31 deletions

View File

@ -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"

View File

@ -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() }

View File

@ -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")

View File

@ -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()
}

View File

@ -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()
) : 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)
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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))
}
}
}
}
}
}

View File

@ -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()

View File

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