mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-29 13:00:17 +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",
|
"name": "Anonymous Story Viewing",
|
||||||
"description": "Prevents anyone from knowing you've seen their story"
|
"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": {
|
"hide_peek_a_peek": {
|
||||||
"name": "Hide Peek-a-Peek",
|
"name": "Hide Peek-a-Peek",
|
||||||
"description": "Prevents notification from being sent when you half swipe into a chat"
|
"description": "Prevents notification from being sent when you half swipe into a chat"
|
||||||
@ -420,6 +424,10 @@
|
|||||||
"name": "Disable Metrics",
|
"name": "Disable Metrics",
|
||||||
"description": "Blocks sending specific analytic data to Snapchat"
|
"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": {
|
"block_ads": {
|
||||||
"name": "Block Ads",
|
"name": "Block Ads",
|
||||||
"description": "Prevents Advertisements from being displayed"
|
"description": "Prevents Advertisements from being displayed"
|
||||||
|
@ -11,6 +11,7 @@ class Global : ConfigContainer() {
|
|||||||
val snapchatPlus = boolean("snapchat_plus") { requireRestart() }
|
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 disableConfirmationDialogs = multiple("disable_confirmation_dialogs", "remove_friend", "block_friend", "ignore_friend", "hide_friend", "hide_conversation", "clear_conversation") { requireRestart() }
|
||||||
val disableMetrics = boolean("disable_metrics")
|
val disableMetrics = boolean("disable_metrics")
|
||||||
|
val disablePublicStories = boolean("disable_public_stories") { requireRestart(); requireCleanCache() }
|
||||||
val blockAds = boolean("block_ads")
|
val blockAds = boolean("block_ads")
|
||||||
val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices(
|
val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices(
|
||||||
FeatureNotice.BAN_RISK); requireRestart(); nativeHooks() }
|
FeatureNotice.BAN_RISK); requireRestart(); nativeHooks() }
|
||||||
|
@ -7,6 +7,7 @@ import me.rhunk.snapenhance.common.data.NotificationType
|
|||||||
class MessagingTweaks : ConfigContainer() {
|
class MessagingTweaks : ConfigContainer() {
|
||||||
val bypassScreenshotDetection = boolean("bypass_screenshot_detection") { requireRestart() }
|
val bypassScreenshotDetection = boolean("bypass_screenshot_detection") { requireRestart() }
|
||||||
val anonymousStoryViewing = boolean("anonymous_story_viewing")
|
val anonymousStoryViewing = boolean("anonymous_story_viewing")
|
||||||
|
val preventStoryRewatchIndicator = boolean("prevent_story_rewatch_indicator") { requireRestart() }
|
||||||
val hidePeekAPeek = boolean("hide_peek_a_peek")
|
val hidePeekAPeek = boolean("hide_peek_a_peek")
|
||||||
val hideBitmojiPresence = boolean("hide_bitmoji_presence")
|
val hideBitmojiPresence = boolean("hide_bitmoji_presence")
|
||||||
val hideTypingNotifications = boolean("hide_typing_notifications")
|
val hideTypingNotifications = boolean("hide_typing_notifications")
|
||||||
|
@ -136,11 +136,13 @@ class EventDispatcher(
|
|||||||
NetworkApiRequestEvent(
|
NetworkApiRequestEvent(
|
||||||
url = request.getObjectField("mUrl") as String,
|
url = request.getObjectField("mUrl") as String,
|
||||||
callback = param.arg(4),
|
callback = param.arg(4),
|
||||||
|
uploadDataProvider = param.argNullable(5),
|
||||||
request = request,
|
request = request,
|
||||||
).apply {
|
).apply {
|
||||||
adapter = param
|
adapter = param
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
if (canceled) param.setResult(null)
|
||||||
request.setObjectField("mUrl", url)
|
request.setObjectField("mUrl", url)
|
||||||
postHookEvent()
|
postHookEvent()
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,93 @@
|
|||||||
package me.rhunk.snapenhance.core.event.events.impl
|
package me.rhunk.snapenhance.core.event.events.impl
|
||||||
|
|
||||||
import me.rhunk.snapenhance.core.event.events.AbstractHookEvent
|
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(
|
class NetworkApiRequestEvent(
|
||||||
val request: Any,
|
val request: Any,
|
||||||
|
val uploadDataProvider: Any?,
|
||||||
val callback: Any,
|
val callback: Any,
|
||||||
var url: String,
|
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.spying.StealthMode
|
||||||
import me.rhunk.snapenhance.core.features.impl.tweaks.CameraTweaks
|
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.tweaks.BypassScreenshotDetection
|
||||||
|
import me.rhunk.snapenhance.core.features.impl.Stories
|
||||||
import me.rhunk.snapenhance.core.features.impl.ui.*
|
import me.rhunk.snapenhance.core.features.impl.ui.*
|
||||||
import me.rhunk.snapenhance.core.logger.CoreLogger
|
import me.rhunk.snapenhance.core.logger.CoreLogger
|
||||||
import me.rhunk.snapenhance.core.manager.Manager
|
import me.rhunk.snapenhance.core.manager.Manager
|
||||||
@ -68,7 +69,6 @@ class FeatureManager(
|
|||||||
StealthMode::class,
|
StealthMode::class,
|
||||||
MenuViewInjector::class,
|
MenuViewInjector::class,
|
||||||
PreventReadReceipts::class,
|
PreventReadReceipts::class,
|
||||||
AnonymousStoryViewing::class,
|
|
||||||
MessageLogger::class,
|
MessageLogger::class,
|
||||||
SnapchatPlus::class,
|
SnapchatPlus::class,
|
||||||
DisableMetrics::class,
|
DisableMetrics::class,
|
||||||
@ -108,6 +108,7 @@ class FeatureManager(
|
|||||||
BypassScreenshotDetection::class,
|
BypassScreenshotDetection::class,
|
||||||
HalfSwipeNotifier::class,
|
HalfSwipeNotifier::class,
|
||||||
DisableConfirmationDialogs::class,
|
DisableConfirmationDialogs::class,
|
||||||
|
Stories::class,
|
||||||
)
|
)
|
||||||
|
|
||||||
initializeFeatures()
|
initializeFeatures()
|
||||||
|
@ -75,8 +75,8 @@ object Hooker {
|
|||||||
methodName: String,
|
methodName: String,
|
||||||
stage: HookStage,
|
stage: HookStage,
|
||||||
crossinline hookConsumer: (HookAdapter) -> Unit
|
crossinline hookConsumer: (HookAdapter) -> Unit
|
||||||
) {
|
): List<() -> Unit> {
|
||||||
val unhooks: MutableSet<XC_MethodHook.Unhook> = HashSet()
|
val unhooks = mutableSetOf<XC_MethodHook.Unhook>()
|
||||||
hook(clazz, methodName, stage) { param->
|
hook(clazz, methodName, stage) { param->
|
||||||
if (param.nullableThisObject<Any>().let {
|
if (param.nullableThisObject<Any>().let {
|
||||||
if (it == null) unhooks.forEach { u -> u.unhook() }
|
if (it == null) unhooks.forEach { u -> u.unhook() }
|
||||||
@ -84,6 +84,9 @@ object Hooker {
|
|||||||
}) return@hook
|
}) return@hook
|
||||||
hookConsumer(param)
|
hookConsumer(param)
|
||||||
}.also { unhooks.addAll(it) }
|
}.also { unhooks.addAll(it) }
|
||||||
|
return unhooks.map {
|
||||||
|
{ it.unhook() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun ephemeralHook(
|
inline fun ephemeralHook(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user