feat(core): stealth mode save snap in chat

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
rhunk
2025-08-26 14:13:26 +02:00
parent 3fb05c764e
commit d4c8a21504
5 changed files with 71 additions and 10 deletions

View File

@@ -1355,7 +1355,8 @@
},
"auto_mark_as_read": {
"conversation_read": "Mark conversation as read when sending a message",
"snap_reply": "Mark snaps as read when replying to them"
"snap_reply": "Mark snaps as read when replying to them",
"save_snap_in_chat": "Mark snaps as read when saving them in chat while in Stealth Mode"
},
"friend_mutation_notifier": {
"remove_friend": "Notify when someone removes you as a friend",

View File

@@ -62,7 +62,7 @@ class MessagingTweaks : ConfigContainer() {
val hideBitmojiPresence = boolean("hide_bitmoji_presence")
val hideTypingNotifications = boolean("hide_typing_notifications")
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read") { requireRestart() }
val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read", "save_snap_in_chat") { requireRestart() }
val markSnapAsSeenButton = boolean("mark_snap_as_seen_button") { requireRestart() }
val skipWhenMarkingAsSeen = boolean("skip_when_marking_as_seen") { requireRestart() }
val loopMediaPlayback = boolean("loop_media_playback") { requireRestart() }

View File

@@ -9,11 +9,17 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.MessageUpdate
import me.rhunk.snapenhance.core.event.events.impl.OnSnapInteractionEvent
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.util.CallbackBuilder
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
@@ -82,6 +88,52 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") {
val config by context.config.messaging.autoMarkAsRead
if (config.isEmpty()) return
if (config.contains("save_snap_in_chat")) {
var lastInteractedSnapClientMessageId = -1L
context.event.subscribe(OnSnapInteractionEvent::class) { event ->
lastInteractedSnapClientMessageId = event.messageId
}
context.classCache.conversationManager.hook("updateMessage", HookStage.BEFORE) { param ->
if (param.arg<Any>(2).toString() != "SAVE") return@hook
val clientMessageId = param.arg<Long>(1)
if (lastInteractedSnapClientMessageId != clientMessageId) return@hook
val conversationId = SnapUUID(param.arg(0))
param.setResult(null)
val snapManager = context.feature(Messaging::class).snapManager ?: return@hook
val stealthMode = context.feature(StealthMode::class)
// ignore non-stealth mode conversations
if (!stealthMode.canUseRule(conversationId.toString())) return@hook
stealthMode.addSnapInteractionException(clientMessageId)
val onSnapInteraction = snapManager.javaClass.methods.firstOrNull { it.name == "onSnapInteraction" } ?: return@hook
context.mappings.useMapper(CallbackMapper::class) {
onSnapInteraction.invoke(snapManager,
findClass("com.snapchat.client.messaging.SnapInteractionType").enumConstants!!.first { it.toString() == "VIEWING_INITIATED" },
conversationId.instanceNonNull(),
clientMessageId,
CallbackBuilder(callbacks.getClass("SnapInteractionCallback")!!)
.override("onSuccess") {
param.invokeOriginal()
stealthMode.addSnapInteractionException(clientMessageId)
}
.override("onError") {
context.log.verbose("error ${it.arg<Any>(0)}")
}
.build()
)
}
}
}
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
event.addCallbackResult("onSuccess") {
if (canMarkConversationAsRead) {

View File

@@ -14,11 +14,7 @@ import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull
import me.rhunk.snapenhance.core.wrapper.impl.ConversationManager
import me.rhunk.snapenhance.core.wrapper.impl.Message
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter
import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID
import me.rhunk.snapenhance.core.wrapper.impl.*
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
import me.rhunk.snapenhance.mapper.impl.FriendsFeedEventDispatcherMapper
import java.util.UUID
@@ -27,6 +23,9 @@ import java.util.concurrent.Future
class Messaging : Feature("Messaging") {
var conversationManager: ConversationManager? = null
private set
var snapManager: Any? = null
private set
private var conversationManagerDelegate: Any? = null
private var identityDelegate: Any? = null
@@ -69,6 +68,10 @@ class Messaging : Feature("Messaging") {
}
}
context.classCache.snapManager.hookConstructor(HookStage.BEFORE) { param ->
snapManager = param.thisObject()
}
context.mappings.useMapper(CallbackMapper::class) {
callbacks.getClass("ConversationManagerDelegate")?.apply {
hookConstructor(HookStage.AFTER) { param ->

View File

@@ -10,15 +10,19 @@ import java.util.concurrent.CopyOnWriteArraySet
class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALTH) {
private val displayedMessageQueue = CopyOnWriteArraySet<Long>()
private val snapInteractionQueue = CopyOnWriteArraySet<Long>()
fun addDisplayedMessageException(clientMessageId: Long) {
displayedMessageQueue.add(clientMessageId)
}
fun addSnapInteractionException(messageId: Long) {
snapInteractionQueue.add(messageId)
}
override fun init() {
val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{
context.feature(StealthMode::class).canUseRule(it.toString())
}
val isConversationInStealthMode: (SnapUUID) -> Boolean = { canUseRule(it.toString()) }
arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String ->
context.classCache.conversationManager.hook(methodName, HookStage.BEFORE) { param ->
@@ -30,6 +34,7 @@ class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALT
}
context.event.subscribe(OnSnapInteractionEvent::class) { event ->
if (snapInteractionQueue.removeIf { event.messageId == it }) return@subscribe
if (isConversationInStealthMode(event.conversationId)) {
event.canceled = true
}