feat: friend tracker experiment

This commit is contained in:
rhunk
2024-02-19 00:42:31 +01:00
parent 80386d6f58
commit 4aab812e5c
17 changed files with 856 additions and 36 deletions

View File

@ -14,9 +14,10 @@ import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.bridge.BridgeInterface
import me.rhunk.snapenhance.bridge.ConfigStateListener
import me.rhunk.snapenhance.bridge.DownloadCallback
import me.rhunk.snapenhance.bridge.MessageLoggerInterface
import me.rhunk.snapenhance.bridge.logger.LoggerInterface
import me.rhunk.snapenhance.bridge.SyncCallback
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface
import me.rhunk.snapenhance.bridge.logger.TrackerInterface
import me.rhunk.snapenhance.bridge.scripting.IScripting
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
import me.rhunk.snapenhance.common.Constants
@ -191,11 +192,13 @@ class BridgeClient(
service.setRule(targetUuid, type.key, state)
}
fun getScriptingInterface(): IScripting = safeServiceCall { service.getScriptingInterface() }
fun getScriptingInterface(): IScripting = safeServiceCall { service.scriptingInterface }
fun getE2eeInterface(): E2eeInterface = safeServiceCall { service.getE2eeInterface() }
fun getE2eeInterface(): E2eeInterface = safeServiceCall { service.e2eeInterface }
fun getMessageLogger(): MessageLoggerInterface = safeServiceCall { service.messageLogger }
fun getMessageLogger(): LoggerInterface = safeServiceCall { service.logger }
fun getTracker(): TrackerInterface = safeServiceCall { service.tracker }
fun registerMessagingBridge(bridge: MessagingBridge) = safeServiceCall { service.registerMessagingBridge(bridge) }

View File

@ -252,6 +252,17 @@ class DatabaseAccess(
}
}
fun getConversationServerMessage(conversationId: String, serverId: Long): ConversationMessage? {
return useDatabase(DatabaseType.ARROYO)?.performOperation {
readDatabaseObject(
ConversationMessage(),
"conversation_message",
"client_conversation_id = ? AND server_message_id = ?",
arrayOf(conversationId, serverId.toString())
)
}
}
fun getConversationType(conversationId: String): Int? {
return useDatabase(DatabaseType.ARROYO)?.performOperation {
safeRawQuery(

View File

@ -1,12 +1,15 @@
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 android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import me.rhunk.snapenhance.common.Constants
import me.rhunk.snapenhance.common.data.*
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.common.util.toParcelable
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor
@ -17,6 +20,43 @@ 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 val tracker by lazy { context.bridgeClient.getTracker() }
private fun getTrackedEvents(eventType: TrackerEventType): TrackerEventsResult? {
return runCatching {
tracker.getTrackedEvents(eventType.key)?.let {
toParcelable<TrackerEventsResult>(it)
}
}.onFailure {
context.log.error("Failed to get tracked events for $eventType", it)
}.getOrNull()
}
private fun isInConversation(conversationId: String) = context.feature(Messaging::class).openedConversationUUID?.toString() == conversationId
private fun sendInfoNotification(id: Int = System.nanoTime().toInt(), text: String) {
context.androidContext.getSystemService(NotificationManager::class.java).notify(
id,
Notification.Builder(
context.androidContext,
"general_group_generic_push_noisy_generic_push_B~LVSD2"
)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setAutoCancel(true)
.setShowWhen(true)
.setWhen(System.currentTimeMillis())
.setContentIntent(context.androidContext.packageManager.getLaunchIntentForPackage(
Constants.SNAPCHAT_PACKAGE_NAME
)?.let {
PendingIntent.getActivity(
context.androidContext,
0, it, PendingIntent.FLAG_IMMUTABLE
)
})
.setContentText(text)
.build()
)
}
private fun handleVolatileEvent(protoReader: ProtoReader) {
context.log.verbose("volatile event\n$protoReader")
@ -24,10 +64,97 @@ class SessionEvents : Feature("Session Events", loadParams = FeatureLoadParams.I
private fun onConversationPresenceUpdate(conversationId: String, userId: String, oldState: FriendPresenceState?, currentState: FriendPresenceState?) {
context.log.verbose("presence state for $userId in conversation $conversationId\n$currentState")
val eventType = when {
(oldState == null || currentState?.bitmojiPresent == false) && currentState?.bitmojiPresent == true -> TrackerEventType.CONVERSATION_ENTER
(currentState == null || oldState?.bitmojiPresent == false) && oldState?.bitmojiPresent == true -> TrackerEventType.CONVERSATION_EXIT
oldState?.typing == false && currentState?.typing == true -> if (currentState.speaking) TrackerEventType.STARTED_SPEAKING else TrackerEventType.STARTED_TYPING
oldState?.typing == true && (currentState == null || !currentState.typing) -> if (oldState.speaking) TrackerEventType.STOPPED_SPEAKING else TrackerEventType.STOPPED_TYPING
(oldState == null || !oldState.peeking) && currentState?.peeking == true -> TrackerEventType.STARTED_PEEKING
oldState?.peeking == true && (currentState == null || !currentState.peeking) -> TrackerEventType.STOPPED_PEEKING
else -> null
} ?: return
val feedEntry = context.database.getFeedEntryByConversationId(conversationId)
val conversationName = feedEntry?.feedDisplayName ?: "DMs"
val authorName = context.database.getFriendInfo(userId)?.mutableUsername ?: "Unknown"
context.log.verbose("$authorName $eventType in $conversationName")
getTrackedEvents(eventType)?.takeIf { it.canTrackOn(conversationId, userId) }?.apply {
if (hasFlags(TrackerFlags.APP_IS_ACTIVE) && context.isMainActivityPaused) return
if (hasFlags(TrackerFlags.APP_IS_INACTIVE) && !context.isMainActivityPaused) return
if (hasFlags(TrackerFlags.IS_IN_CONVERSATION) && !isInConversation(conversationId)) return
if (hasFlags(TrackerFlags.NOTIFY)) sendInfoNotification(text = "$authorName $eventType in $conversationName")
if (hasFlags(TrackerFlags.LOG)) {
context.bridgeClient.getMessageLogger().logTrackerEvent(
conversationId,
conversationName,
context.database.getConversationType(conversationId) == 1,
authorName,
userId,
eventType.key,
""
)
}
}
}
private fun onConversationMessagingEvent(event: SessionEvent) {
context.log.verbose("conversation messaging event\n${event.type} in ${event.conversationId} from ${event.authorUserId}")
val isConversationGroup = context.database.getConversationType(event.conversationId) == 1
val authorName = context.database.getFriendInfo(event.authorUserId)?.mutableUsername ?: "Unknown"
val conversationName = context.database.getFeedEntryByConversationId(event.conversationId)?.feedDisplayName ?: "DMs"
val conversationMessage by lazy {
(event as? SessionMessageEvent)?.serverMessageId?.let { context.database.getConversationServerMessage(event.conversationId, it) }
}
val eventType = when(event.type) {
SessionEventType.MESSAGE_READ_RECEIPTS -> TrackerEventType.MESSAGE_READ
SessionEventType.MESSAGE_DELETED -> TrackerEventType.MESSAGE_DELETED
SessionEventType.MESSAGE_REACTION_ADD -> TrackerEventType.MESSAGE_REACTION_ADD
SessionEventType.MESSAGE_REACTION_REMOVE -> TrackerEventType.MESSAGE_REACTION_REMOVE
SessionEventType.MESSAGE_SAVED -> TrackerEventType.MESSAGE_SAVED
SessionEventType.MESSAGE_UNSAVED -> TrackerEventType.MESSAGE_UNSAVED
SessionEventType.SNAP_OPENED -> TrackerEventType.SNAP_OPENED
SessionEventType.SNAP_REPLAYED -> TrackerEventType.SNAP_REPLAYED
SessionEventType.SNAP_REPLAYED_TWICE -> TrackerEventType.SNAP_REPLAYED_TWICE
SessionEventType.SNAP_SCREENSHOT -> TrackerEventType.SNAP_SCREENSHOT
SessionEventType.SNAP_SCREEN_RECORD -> TrackerEventType.SNAP_SCREEN_RECORD
else -> return
}
val messageEvents = arrayOf(
TrackerEventType.MESSAGE_READ,
TrackerEventType.MESSAGE_DELETED,
TrackerEventType.MESSAGE_REACTION_ADD,
TrackerEventType.MESSAGE_REACTION_REMOVE,
TrackerEventType.MESSAGE_SAVED,
TrackerEventType.MESSAGE_UNSAVED
)
getTrackedEvents(eventType)?.takeIf { it.canTrackOn(event.conversationId, event.authorUserId) }?.apply {
if (messageEvents.contains(eventType) && conversationMessage?.senderId == context.database.myUserId) return
if (hasFlags(TrackerFlags.APP_IS_ACTIVE) && context.isMainActivityPaused) return
if (hasFlags(TrackerFlags.APP_IS_INACTIVE) && !context.isMainActivityPaused) return
if (hasFlags(TrackerFlags.IS_IN_CONVERSATION) && !isInConversation(event.conversationId)) return
if (hasFlags(TrackerFlags.NOTIFY)) sendInfoNotification(text = "$authorName $eventType in $conversationName")
if (hasFlags(TrackerFlags.LOG)) {
context.bridgeClient.getMessageLogger().logTrackerEvent(
event.conversationId,
conversationName,
isConversationGroup,
authorName,
event.authorUserId,
eventType.key,
messageEvents.takeIf { it.contains(eventType) }?.let {
conversationMessage?.contentType?.let { ContentType.fromId(it) } ?.name
} ?: ""
)
}
}
}
private fun handlePresenceEvent(protoReader: ProtoReader) {
@ -66,7 +193,7 @@ class SessionEvents : Feature("Session Events", loadParams = FeatureLoadParams.I
private fun handleMessagingEvent(protoReader: ProtoReader) {
// read receipts
protoReader.followPath(12) {
val conversationId = getByteArray(1, 1)?.toSnapUUID().toString() ?: return@followPath
val conversationId = getByteArray(1, 1)?.toSnapUUID()?.toString() ?: return@followPath
followPath(7) readReceipts@{
val senderId = getByteArray(1, 1)?.toSnapUUID()?.toString() ?: return@readReceipts
@ -84,8 +211,8 @@ class SessionEvents : Feature("Session Events", loadParams = FeatureLoadParams.I
}
protoReader.followPath(6, 2) {
val conversationId = getByteArray(1, 1)?.toSnapUUID()?.toString() ?: return@followPath
val senderId = getByteArray(3, 1)?.toSnapUUID()?.toString() ?: return@followPath
val conversationId = getByteArray(3, 1)?.toSnapUUID()?.toString() ?: return@followPath
val senderId = getByteArray(1, 1)?.toSnapUUID()?.toString() ?: return@followPath
val serverMessageId = getVarInt(2) ?: return@followPath
if (contains(4)) {

View File

@ -33,7 +33,7 @@ class MessageLogger : Feature("MessageLogger",
const val DELETED_MESSAGE_COLOR = 0x6Eb71c1c
}
private val messageLoggerInterface by lazy { context.bridgeClient.getMessageLogger() }
private val loggerInterface by lazy { context.bridgeClient.getMessageLogger() }
val isEnabled get() = context.config.messaging.messageLogger.globalState == true
@ -50,7 +50,7 @@ class MessageLogger : Feature("MessageLogger",
val uniqueMessageId = makeUniqueIdentifier(conversationId, clientMessageId) ?: return
fetchedMessages.remove(uniqueMessageId)
deletedMessageCache.remove(uniqueMessageId)
messageLoggerInterface.deleteMessage(conversationId, uniqueMessageId)
loggerInterface.deleteMessage(conversationId, uniqueMessageId)
}
fun getMessageObject(conversationId: String, clientMessageId: Long): JsonObject? {
@ -58,7 +58,7 @@ class MessageLogger : Feature("MessageLogger",
if (deletedMessageCache.containsKey(uniqueMessageId)) {
return deletedMessageCache[uniqueMessageId]
}
return messageLoggerInterface.getMessage(conversationId, uniqueMessageId)?.let {
return loggerInterface.getMessage(conversationId, uniqueMessageId)?.let {
JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject
}
}
@ -93,7 +93,7 @@ class MessageLogger : Feature("MessageLogger",
measureTimeMillis {
val conversationIds = context.database.getFeedEntries(PREFETCH_FEED_COUNT).map { it.key!! }
if (conversationIds.isEmpty()) return@measureTimeMillis
fetchedMessages.addAll(messageLoggerInterface.getLoggedIds(conversationIds.toTypedArray(), PREFETCH_MESSAGE_COUNT).toList())
fetchedMessages.addAll(loggerInterface.getLoggedIds(conversationIds.toTypedArray(), PREFETCH_MESSAGE_COUNT).toList())
}.also { context.log.verbose("Loaded ${fetchedMessages.size} cached messages in ${it}ms") }
}
@ -124,7 +124,7 @@ class MessageLogger : Feature("MessageLogger",
threadPool.execute {
try {
messageLoggerInterface.addMessage(conversationId, uniqueMessageIdentifier, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8))
loggerInterface.addMessage(conversationId, uniqueMessageIdentifier, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8))
} catch (ignored: DeadObjectException) {}
}
@ -135,7 +135,7 @@ class MessageLogger : Feature("MessageLogger",
val deletedMessageObject: JsonObject = if (deletedMessageCache.containsKey(uniqueMessageIdentifier))
deletedMessageCache[uniqueMessageIdentifier]
else {
messageLoggerInterface.getMessage(conversationId, uniqueMessageIdentifier)?.let {
loggerInterface.getMessage(conversationId, uniqueMessageIdentifier)?.let {
JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject
}
} ?: return@subscribe