mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-12 13:17:42 +02:00
feat: friend tracker experiment
This commit is contained in:
@ -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) }
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)) {
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user