feat(experimental): session events

- refactor SnapUUID
This commit is contained in:
rhunk
2024-02-13 22:55:30 +01:00
parent 08c9d46858
commit 60ee3680a4
16 changed files with 374 additions and 42 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -118,6 +118,7 @@ class FeatureManager(
OperaViewerParamsOverride(),
StealthModeIndicator(),
DisablePermissionRequests(),
SessionEvents(),
)
initializeFeatures()

View File

@ -23,7 +23,7 @@ class CoreMessaging(
fun isPresent() = conversationManager != null
@JSFunction
fun newSnapUUID(uuid: String) = SnapUUID.fromString(uuid)
fun newSnapUUID(uuid: String) = SnapUUID(uuid)
@JSFunction
fun updateMessage(
@ -143,7 +143,7 @@ class CoreMessaging(
message: String,
result: (error: String?) -> Unit
) {
modContext.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), message, onSuccess = { result(null) }, onError = { result(it.toString()) })
modContext.messageSender.sendChatMessage(listOf(SnapUUID(conversationId)), message, onSuccess = { result(null) }, onError = { result(it.toString()) })
}
@JSFunction

View File

@ -6,7 +6,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import kotlin.reflect.KProperty
abstract class AbstractWrapper(
protected var instance: Any?
protected open var instance: Any?
) {
protected val uuidArrayListMapper: (Any?) -> ArrayList<SnapUUID> get() = { (it as ArrayList<*>).map { i -> SnapUUID(i) }.toCollection(ArrayList()) }

View File

@ -40,7 +40,7 @@ class ConversationManager(
fun updateMessage(conversationId: String, messageId: Long, action: MessageUpdate, onResult: CallbackResult = {}) {
updateMessageMethod.invoke(
instanceNonNull(),
SnapUUID.fromString(conversationId).instanceNonNull(),
SnapUUID(conversationId).instanceNonNull(),
messageId,
context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == action.toString() },
CallbackBuilder(getCallbackClass("Callback"))

View File

@ -6,41 +6,53 @@ import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
import java.nio.ByteBuffer
import java.util.UUID
fun String.toSnapUUID() = SnapUUID.fromString(this)
fun String.toSnapUUID() = SnapUUID(this)
fun ByteArray.toSnapUUID() = SnapUUID(this)
class SnapUUID(obj: Any?) : AbstractWrapper(obj) {
private val uuidString by lazy { toUUID().toString() }
private val bytes: ByteArray get() = instanceNonNull().getObjectField("mId") as ByteArray
private fun toUUID(): UUID {
val buffer = ByteBuffer.wrap(bytes)
return UUID(buffer.long, buffer.long)
fun UUID.toBytes(): ByteArray =
ByteBuffer.allocate(16).let {
it.putLong(this.mostSignificantBits)
it.putLong(this.leastSignificantBits)
it.array()
}
class SnapUUID(
private val obj: Any?
) : AbstractWrapper(obj) {
private val uuidBytes by lazy {
when {
obj is String -> {
UUID.fromString(obj).toBytes()
}
obj is ByteArray -> {
assert(obj.size == 16)
obj
}
obj is UUID -> obj.toBytes()
SnapEnhance.classCache.snapUUID.isInstance(obj) -> {
obj?.getObjectField("mId") as ByteArray
}
else -> ByteArray(16)
}
}
private val uuidString by lazy { ByteBuffer.wrap(uuidBytes).run { UUID(long, long) }.toString() }
override var instance: Any?
set(_) {}
get() = SnapEnhance.classCache.snapUUID.getConstructor(ByteArray::class.java).newInstance(uuidBytes)
override fun toString(): String {
return uuidString
}
fun toBytes() = bytes
fun toBytes() = uuidBytes
override fun equals(other: Any?): Boolean {
return other is SnapUUID && other.uuidString == uuidString
return other is SnapUUID && other.uuidBytes.contentEquals(this.uuidBytes)
}
companion object {
fun fromString(uuid: String): SnapUUID {
return fromUUID(UUID.fromString(uuid))
}
fun fromBytes(bytes: ByteArray): SnapUUID {
val constructor = SnapEnhance.classCache.snapUUID.getConstructor(ByteArray::class.java)
return SnapUUID(constructor.newInstance(bytes))
}
fun fromUUID(uuid: UUID): SnapUUID {
val buffer = ByteBuffer.allocate(16)
buffer.putLong(uuid.mostSignificantBits)
buffer.putLong(uuid.leastSignificantBits)
return fromBytes(buffer.array())
}
override fun hashCode(): Int {
return uuidBytes.contentHashCode()
}
}