mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-12 13:17:42 +02:00
feat(experimental): session events
- refactor SnapUUID
This commit is contained in:
@ -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) {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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) }) {
|
||||
|
@ -118,6 +118,7 @@ class FeatureManager(
|
||||
OperaViewerParamsOverride(),
|
||||
StealthModeIndicator(),
|
||||
DisablePermissionRequests(),
|
||||
SessionEvents(),
|
||||
)
|
||||
|
||||
initializeFeatures()
|
||||
|
@ -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
|
||||
|
@ -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()) }
|
||||
|
||||
|
@ -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"))
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user