mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-28 04:20:20 +02:00
feat: story features
- disable rewatch indicator - disable public stories
This commit is contained in:
parent
7d4963770d
commit
e9b9a71a7e
@ -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"
|
||||
|
@ -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() }
|
||||
|
@ -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")
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user