Skip to content

Commit

Permalink
feat(experimental): session events
Browse files Browse the repository at this point in the history
- refactor SnapUUID
  • Loading branch information
rhunk committed Feb 13, 2024
1 parent 08c9d46 commit 60ee368
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 41 deletions.
14 changes: 14 additions & 0 deletions common/src/main/assets/lang/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,20 @@
}
}
},
"session_events": {
"name": "Session Events",
"description": "Records session events",
"properties": {
"capture_duplex_events": {
"name": "Capture Duplex Events",
"description": "Capture presence and messaging events when a session is active"
},
"allow_running_in_background": {
"name": "Allow Running in Background",
"description": "Allows session to run in the background"
}
}
},
"spoof": {
"name": "Spoof",
"description": "Spoof various information about you",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@ import me.rhunk.snapenhance.common.config.ConfigContainer
import me.rhunk.snapenhance.common.config.FeatureNotice

class Experimental : ConfigContainer() {
class SessionEventsConfig : ConfigContainer(hasGlobalState = true) {
val captureDuplexEvents = boolean("capture_duplex_events", true)
val allowRunningInBackground = boolean("allow_running_in_background", true)
}

class NativeHooks : ConfigContainer(hasGlobalState = true) {
val disableBitmoji = boolean("disable_bitmoji")
}

class E2EEConfig : ConfigContainer(hasGlobalState = true) {
val encryptedMessageIndicator = boolean("encrypted_message_indicator")
val forceMessageEncryption = boolean("force_message_encryption")
}

val nativeHooks = container("native_hooks", NativeHooks()) { icon = "Memory"; requireRestart() }
val sessionEvents = container("session_events", SessionEventsConfig()) { requireRestart(); nativeHooks() }
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" ; addNotices(FeatureNotice.BAN_RISK); requireRestart() }
val convertMessageLocally = boolean("convert_message_locally") { requireRestart() }
val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package me.rhunk.snapenhance.common.data


data class FriendPresenceState(
val bitmojiPresent: Boolean,
val typing: Boolean,
val wasTyping: Boolean,
val speaking: Boolean,
val peeking: Boolean
)

open class SessionEvent(
val type: SessionEventType,
val conversationId: String,
val authorUserId: String,
)

class SessionMessageEvent(
type: SessionEventType,
conversationId: String,
authorUserId: String,
val serverMessageId: Long,
val messageData: ByteArray? = null,
val reactionId: Int? = null,
) : SessionEvent(type, conversationId, authorUserId)


enum class SessionEventType(
val key: String
) {
MESSAGE_READ_RECEIPTS("message_read_receipts"),
MESSAGE_DELETED("message_deleted"),
MESSAGE_SAVED("message_saved"),
MESSAGE_UNSAVED("message_unsaved"),
MESSAGE_REACTION_ADD("message_reaction_add"),
MESSAGE_REACTION_REMOVE("message_reaction_remove"),
SNAP_OPENED("snap_opened"),
SNAP_REPLAYED("snap_replayed"),
SNAP_REPLAYED_TWICE("snap_replayed_twice"),
SNAP_SCREENSHOT("snap_screenshot"),
SNAP_SCREEN_RECORD("snap_screen_record"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class EndToEndEncryption : MessagingRuleFeature(

private fun sendCustomMessage(conversationId: String, messageId: Int, message: ProtoWriter.() -> Unit) {
context.messageSender.sendCustomChatMessage(
listOf(SnapUUID.fromString(conversationId)),
listOf(SnapUUID(conversationId)),
ContentType.CHAT,
message = {
from(2) {
Expand Down Expand Up @@ -444,7 +444,7 @@ class EndToEndEncryption : MessagingRuleFeature(
hasStory = true
return@eachBuffer
}
conversationIds.add(SnapUUID.fromBytes(getByteArray(1, 1, 1) ?: return@eachBuffer))
conversationIds.add(SnapUUID(getByteArray(1, 1, 1) ?: return@eachBuffer))
}

if (hasStory) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package me.rhunk.snapenhance.core.features.impl.experiments

import me.rhunk.snapenhance.common.data.SessionMessageEvent
import me.rhunk.snapenhance.common.data.SessionEvent
import me.rhunk.snapenhance.common.data.SessionEventType
import me.rhunk.snapenhance.common.data.FriendPresenceState
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor
import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID
import me.rhunk.snapenhance.nativelib.NativeLib
import java.lang.reflect.Method
import java.nio.ByteBuffer

class SessionEvents : Feature("Session Events", loadParams = FeatureLoadParams.INIT_SYNC) {
private val conversationPresenceState = mutableMapOf<String, MutableMap<String, FriendPresenceState?>>() // conversationId -> (userId -> state)

private fun handleVolatileEvent(protoReader: ProtoReader) {
context.log.verbose("volatile event\n$protoReader")
}

private fun onConversationPresenceUpdate(conversationId: String, userId: String, oldState: FriendPresenceState?, currentState: FriendPresenceState?) {
context.log.verbose("presence state for $userId in conversation $conversationId\n$currentState")
}

private fun onConversationMessagingEvent(event: SessionEvent) {
context.log.verbose("conversation messaging event\n${event.type} in ${event.conversationId} from ${event.authorUserId}")
}

private fun handlePresenceEvent(protoReader: ProtoReader) {
val conversationId = protoReader.getString(6) ?: return

val presenceMap = conversationPresenceState.getOrPut(conversationId) { mutableMapOf() }.toMutableMap()
val userIds = mutableSetOf<String>()

protoReader.eachBuffer(4) {
val participantUserId = getString(1)?.takeIf { it.contains(":") }?.substringBefore(":") ?: return@eachBuffer
userIds.add(participantUserId)
if (participantUserId == context.database.myUserId) return@eachBuffer
val stateMap = getVarInt(2, 1)?.toString(2)?.padStart(16, '0')?.reversed()?.map { it == '1' } ?: return@eachBuffer

presenceMap[participantUserId] = FriendPresenceState(
bitmojiPresent = stateMap[0],
typing = stateMap[4],
wasTyping = stateMap[5],
speaking = stateMap[6] && stateMap[4],
peeking = stateMap[8]
)
}

presenceMap.keys.filterNot { it in userIds }.forEach { presenceMap[it] = null }

presenceMap.forEach { (userId, state) ->
val oldState = conversationPresenceState[conversationId]?.get(userId)
if (oldState != state) {
onConversationPresenceUpdate(conversationId, userId, oldState, state)
}
}

conversationPresenceState[conversationId] = presenceMap
}

private fun handleMessagingEvent(protoReader: ProtoReader) {
// read receipts
protoReader.followPath(12) {
val conversationId = getByteArray(1, 1)?.toSnapUUID().toString() ?: return@followPath

followPath(7) readReceipts@{
val senderId = getByteArray(1, 1)?.toSnapUUID()?.toString() ?: return@readReceipts
val serverMessageId = getVarInt(2, 2) ?: return@readReceipts

onConversationMessagingEvent(
SessionMessageEvent(
SessionEventType.MESSAGE_READ_RECEIPTS,
conversationId,
senderId,
serverMessageId,
)
)
}
}

protoReader.followPath(6, 2) {
val conversationId = getByteArray(1, 1)?.toSnapUUID()?.toString() ?: return@followPath
val senderId = getByteArray(3, 1)?.toSnapUUID()?.toString() ?: return@followPath
val serverMessageId = getVarInt(2) ?: return@followPath

if (contains(4)) {
onConversationMessagingEvent(
SessionMessageEvent(
SessionEventType.SNAP_OPENED,
conversationId,
senderId,
serverMessageId
)
)
}

if (contains(13)) {
onConversationMessagingEvent(
SessionMessageEvent(
if (getVarInt(13, 1) == 2L) SessionEventType.SNAP_REPLAYED_TWICE else SessionEventType.SNAP_REPLAYED,
conversationId,
senderId,
serverMessageId
)
)
}

if (contains(6) || contains(7)) {
onConversationMessagingEvent(
SessionMessageEvent(
if (contains(6)) SessionEventType.MESSAGE_SAVED else SessionEventType.MESSAGE_UNSAVED,
conversationId,
senderId,
serverMessageId
)
)
}

if (contains(11) || contains(12)) {
onConversationMessagingEvent(
SessionMessageEvent(
if (contains(11)) SessionEventType.SNAP_SCREENSHOT else SessionEventType.SNAP_SCREEN_RECORD,
conversationId,
senderId,
serverMessageId,
)
)
}

followPath(16) {
onConversationMessagingEvent(
SessionMessageEvent(
SessionEventType.MESSAGE_REACTION_ADD, conversationId, senderId, serverMessageId, reactionId = getVarInt(1, 1, 1)?.toInt() ?: -1
)
)
}

if (contains(17)) {
onConversationMessagingEvent(
SessionMessageEvent(SessionEventType.MESSAGE_REACTION_REMOVE, conversationId, senderId, serverMessageId)
)
}

followPath(8) {
onConversationMessagingEvent(
SessionMessageEvent(SessionEventType.MESSAGE_DELETED, conversationId, senderId, serverMessageId, messageData = getByteArray(1))
)
}
}
}

override fun init() {
val sessionEventsConfig = context.config.experimental.sessionEvents
if (sessionEventsConfig.globalState != true) return

if (sessionEventsConfig.allowRunningInBackground.get()) {
findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply {
// prevent disabling events when the app is inactive
hook("appStateChanged", HookStage.BEFORE) { param ->
if (param.arg<Any>(0).toString() == "INACTIVE") param.setResult(null)
}
// allow events when a notification is received
hookConstructor(HookStage.AFTER) { param ->
methods.first { it.name == "appStateChanged" }.let { method ->
method.invoke(param.thisObject(), method.parameterTypes[0].enumConstants.first { it.toString() == "ACTIVE" })
}
}
}
}

if (sessionEventsConfig.captureDuplexEvents.get()) {
val messageHandlerClass = findClass("com.snapchat.client.duplex.MessageHandler\$CppProxy").apply {
hook("onReceive", HookStage.BEFORE) { param ->
param.setResult(null)

val byteBuffer = param.arg<ByteBuffer>(0)
val content = byteBuffer.let {
val bytes = ByteArray(it.limit())
it.get(bytes)
bytes
}
val reader = ProtoReader(content)
reader.getString(1, 1)?.let {
val eventData = reader.followPath(1, 2) ?: return@let
if (it == "volatile") {
handleVolatileEvent(eventData)
return@hook
}

if (it == "presence") {
handlePresenceEvent(eventData)
return@hook
}
}
handleMessagingEvent(reader)
}
hook("nativeDestroy", HookStage.BEFORE) { it.setResult(null) }
}


findClass("com.snapchat.client.messaging.Session").hook("create", HookStage.BEFORE) { param ->
if (!NativeLib.initialized) {
context.log.warn("Can't register duplex message handler, native lib not initialized")
return@hook
}

val method = param.method() as Method
val duplexClient = method.parameterTypes.indexOfFirst { it.name.endsWith("DuplexClient") }.let {
param.arg<Any>(it)
}
val dispatchQueue = method.parameterTypes.indexOfFirst { it.name.endsWith("DispatchQueue") }.let {
param.arg<Any>(it)
}
for (channel in arrayOf("pcs", "mcs")) {
duplexClient::class.java.methods.first {
it.name == "registerHandler"
}.invoke(
duplexClient,
channel,
messageHandlerClass.declaredConstructors.first().also { it.isAccessible = true }.newInstance(-1),
dispatchQueue
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
if (it.startsWith("null")) return@hook
}
context.database.getConversationType(conversationId)?.takeIf { it == 1 }?.run {
lastFetchGroupConversationUUID = SnapUUID.fromString(conversationId)
lastFetchGroupConversationUUID = SnapUUID(conversationId)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
.toString()
val myUser = context.database.myUserId.let { context.database.getFriendInfo(it) } ?: return@subscribe

context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = {
context.messageSender.sendChatMessage(listOf(SnapUUID(conversationId)), input, onError = {
context.longToast("Failed to send message: $it")
context.coroutineScope.launch(coroutineDispatcher) {
appendNotificationText("Failed to send message: $it")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class UnsaveableMessages : MessagingRuleFeature(
if (contains(2)) {
return@eachBuffer
}
conversationIds.add(SnapUUID.fromBytes(getByteArray(1, 1, 1) ?: return@eachBuffer).toString())
conversationIds.add(SnapUUID(getByteArray(1, 1, 1) ?: return@eachBuffer).toString())
}

if (conversationIds.all { canUseRule(it) }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class FeatureManager(
OperaViewerParamsOverride(),
StealthModeIndicator(),
DisablePermissionRequests(),
SessionEvents(),
)

initializeFeatures()
Expand Down
Loading

0 comments on commit 60ee368

Please sign in to comment.