From 4aab812e5c161780ec5e08ea4e9a82e8ff5d1f43 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 19 Feb 2024 00:42:31 +0100 Subject: [PATCH] feat: friend tracker experiment --- .../me/rhunk/snapenhance/RemoteSideContext.kt | 6 +- .../me/rhunk/snapenhance/RemoteTracker.kt | 29 ++ .../rhunk/snapenhance/bridge/BridgeService.kt | 3 +- .../snapenhance/messaging/ModDatabase.kt | 106 +++++- .../me/rhunk/snapenhance/ui/manager/Routes.kt | 2 + .../manager/pages/FriendTrackerManagerRoot.kt | 330 ++++++++++++++++++ .../ui/manager/pages/LoggerHistoryRoot.kt | 10 +- .../ui/manager/pages/home/HomeRoot.kt | 12 + .../snapenhance/bridge/BridgeInterface.aidl | 7 +- .../LoggerInterface.aidl} | 14 +- .../bridge/logger/TrackerInterface.aidl | 5 + ...ssageLoggerWrapper.kt => LoggerWrapper.kt} | 99 +++++- .../common/data/SessionEventsData.kt | 94 +++++ .../snapenhance/core/bridge/BridgeClient.kt | 11 +- .../core/database/DatabaseAccess.kt | 11 + .../impl/experiments/SessionEvents.kt | 141 +++++++- .../features/impl/spying/MessageLogger.kt | 12 +- 17 files changed, 856 insertions(+), 36 deletions(-) create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/RemoteTracker.kt create mode 100644 app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/FriendTrackerManagerRoot.kt rename common/src/main/aidl/me/rhunk/snapenhance/bridge/{MessageLoggerInterface.aidl => logger/LoggerInterface.aidl} (73%) create mode 100644 common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/TrackerInterface.aidl rename common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/{MessageLoggerWrapper.kt => LoggerWrapper.kt} (69%) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt index a102fc77..6043f615 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -23,8 +23,8 @@ import me.rhunk.snapenhance.bridge.BridgeService import me.rhunk.snapenhance.common.BuildConfig import me.rhunk.snapenhance.common.bridge.types.BridgeFileType import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.common.bridge.wrapper.LoggerWrapper import me.rhunk.snapenhance.common.bridge.wrapper.MappingsWrapper -import me.rhunk.snapenhance.common.bridge.wrapper.MessageLoggerWrapper import me.rhunk.snapenhance.common.config.ModConfig import me.rhunk.snapenhance.e2ee.E2EEImplementation import me.rhunk.snapenhance.messaging.ModDatabase @@ -69,7 +69,8 @@ class RemoteSideContext( val scriptManager = RemoteScriptManager(this) val settingsOverlay = SettingsOverlay(this) val e2eeImplementation = E2EEImplementation(this) - val messageLogger by lazy { MessageLoggerWrapper(androidContext.getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)) } + val messageLogger by lazy { LoggerWrapper(androidContext.getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)) } + val tracker = RemoteTracker(this) //used to load bitmoji selfies and download previews val imageLoader by lazy { @@ -108,6 +109,7 @@ class RemoteSideContext( streaksReminder.init() scriptManager.init() messageLogger.init() + tracker.init() config.root.messaging.messageLogger.takeIf { it.globalState == true }?.getAutoPurgeTime()?.let { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteTracker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteTracker.kt new file mode 100644 index 00000000..b911cd71 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteTracker.kt @@ -0,0 +1,29 @@ +package me.rhunk.snapenhance + +import me.rhunk.snapenhance.bridge.logger.TrackerInterface +import me.rhunk.snapenhance.common.data.TrackerEventsResult +import me.rhunk.snapenhance.common.data.TrackerRule +import me.rhunk.snapenhance.common.data.TrackerRuleEvent +import me.rhunk.snapenhance.common.util.toSerialized + + +class RemoteTracker( + private val context: RemoteSideContext +): TrackerInterface.Stub() { + fun init() { + /*TrackerEventType.entries.forEach { eventType -> + val ruleId = context.modDatabase.addTrackerRule(TrackerFlags.TRACK or TrackerFlags.LOG or TrackerFlags.NOTIFY, null, null) + context.modDatabase.addTrackerRuleEvent(ruleId, TrackerFlags.TRACK or TrackerFlags.LOG or TrackerFlags.NOTIFY, eventType.key) + }*/ + } + + override fun getTrackedEvents(eventType: String): String? { + val events = mutableMapOf>() + + context.modDatabase.getTrackerEvents(eventType).forEach { (event, rule) -> + events.getOrPut(rule) { mutableListOf() }.add(event) + } + + return TrackerEventsResult(events).toSerialized() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt index 34d4d9eb..b4477f7e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -176,7 +176,8 @@ class BridgeService : Service() { override fun getScriptingInterface() = remoteSideContext.scriptManager override fun getE2eeInterface() = remoteSideContext.e2eeImplementation - override fun getMessageLogger() = remoteSideContext.messageLogger + override fun getLogger() = remoteSideContext.messageLogger + override fun getTracker() = remoteSideContext.tracker override fun registerMessagingBridge(bridge: MessagingBridge) { messagingBridge = bridge } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt index e09007f2..febdfcc8 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -1,17 +1,17 @@ package me.rhunk.snapenhance.messaging +import android.content.ContentValues import android.database.sqlite.SQLiteDatabase +import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.RemoteSideContext -import me.rhunk.snapenhance.common.data.FriendStreaks -import me.rhunk.snapenhance.common.data.MessagingFriendInfo -import me.rhunk.snapenhance.common.data.MessagingGroupInfo -import me.rhunk.snapenhance.common.data.MessagingRuleType +import me.rhunk.snapenhance.common.data.* import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.util.SQLiteDatabaseHelper import me.rhunk.snapenhance.common.util.ktx.getInteger import me.rhunk.snapenhance.common.util.ktx.getLongOrNull import me.rhunk.snapenhance.common.util.ktx.getStringOrNull import java.util.concurrent.Executors +import kotlin.coroutines.suspendCoroutine class ModDatabase( @@ -67,6 +67,18 @@ class ModDatabase( "description VARCHAR", "author VARCHAR NOT NULL", "enabled BOOLEAN" + ), + "tracker_rules" to listOf( + "id INTEGER PRIMARY KEY AUTOINCREMENT", + "flags INTEGER", + "conversation_id CHAR(36)", // nullable + "user_id CHAR(36)", // nullable + ), + "tracker_rules_events" to listOf( + "id INTEGER PRIMARY KEY AUTOINCREMENT", + "flags INTEGER", + "rule_id INTEGER", + "event_type VARCHAR", ) )) } @@ -333,4 +345,90 @@ class ModDatabase( } } } + + fun addTrackerRule(flags: Int, conversationId: String?, userId: String?): Int { + return runBlocking { + suspendCoroutine { continuation -> + executeAsync { + val id = database.insert("tracker_rules", null, ContentValues().apply { + put("flags", flags) + put("conversation_id", conversationId) + put("user_id", userId) + }) + continuation.resumeWith(Result.success(id.toInt())) + } + } + } + } + + fun addTrackerRuleEvent(ruleId: Int, flags: Int, eventType: String) { + executeAsync { + database.execSQL("INSERT INTO tracker_rules_events (flags, rule_id, event_type) VALUES (?, ?, ?)", arrayOf( + flags, + ruleId, + eventType + )) + } + } + + fun getTrackerRules(conversationId: String?, userId: String?): List { + val rules = mutableListOf() + + database.rawQuery("SELECT * FROM tracker_rules WHERE (conversation_id = ? OR conversation_id IS NULL) AND (user_id = ? OR user_id IS NULL)", arrayOf(conversationId, userId).filterNotNull().toTypedArray()).use { cursor -> + while (cursor.moveToNext()) { + rules.add( + TrackerRule( + id = cursor.getInteger("id"), + flags = cursor.getInteger("flags"), + conversationId = cursor.getStringOrNull("conversation_id"), + userId = cursor.getStringOrNull("user_id") + ) + ) + } + } + + return rules + } + + fun getTrackerEvents(ruleId: Int): List { + val events = mutableListOf() + database.rawQuery("SELECT * FROM tracker_rules_events WHERE rule_id = ?", arrayOf(ruleId.toString())).use { cursor -> + while (cursor.moveToNext()) { + events.add( + TrackerRuleEvent( + id = cursor.getInteger("id"), + flags = cursor.getInteger("flags"), + eventType = cursor.getStringOrNull("event_type") ?: continue + ) + ) + } + } + return events + } + + fun getTrackerEvents(eventType: String): Map { + val events = mutableMapOf() + database.rawQuery("SELECT tracker_rules_events.id as event_id, tracker_rules_events.flags, tracker_rules_events.event_type, tracker_rules.conversation_id, tracker_rules.user_id " + + "FROM tracker_rules_events " + + "INNER JOIN tracker_rules " + + "ON tracker_rules_events.rule_id = tracker_rules.id " + + "WHERE event_type = ?", arrayOf(eventType) + ).use { cursor -> + while (cursor.moveToNext()) { + val trackerRule = TrackerRule( + id = -1, + flags = cursor.getInteger("flags"), + conversationId = cursor.getStringOrNull("conversation_id"), + userId = cursor.getStringOrNull("user_id") + ) + val trackerRuleEvent = TrackerRuleEvent( + id = cursor.getInteger("event_id"), + flags = cursor.getInteger("flags"), + eventType = cursor.getStringOrNull("event_type") ?: continue + ) + events[trackerRuleEvent] = trackerRule + } + } + return events + } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt index f954732e..077e17b9 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt @@ -15,6 +15,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraphBuilder import me.rhunk.snapenhance.RemoteSideContext +import me.rhunk.snapenhance.ui.manager.pages.FriendTrackerManagerRoot import me.rhunk.snapenhance.ui.manager.pages.LoggerHistoryRoot import me.rhunk.snapenhance.ui.manager.pages.TasksRoot import me.rhunk.snapenhance.ui.manager.pages.features.FeaturesRoot @@ -53,6 +54,7 @@ class Routes( val settings = route(RouteInfo("home_settings"), HomeSettings()).parent(home) val homeLogs = route(RouteInfo("home_logs"), HomeLogs()).parent(home) val loggerHistory = route(RouteInfo("logger_history"), LoggerHistoryRoot()).parent(home) + val friendTracker = route(RouteInfo("friend_tracker"), FriendTrackerManagerRoot()).parent(home) val social = route(RouteInfo("social", icon = Icons.Default.Group, primary = true), SocialRoot()) val manageScope = route(RouteInfo("manage_scope/?scope={scope}&id={id}"), ManageScope()).parent(social) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/FriendTrackerManagerRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/FriendTrackerManagerRoot.kt new file mode 100644 index 00000000..3abf844f --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/FriendTrackerManagerRoot.kt @@ -0,0 +1,330 @@ +package me.rhunk.snapenhance.ui.manager.pages + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.DeleteOutline +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.PopupProperties +import androidx.navigation.NavBackStackEntry +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.common.bridge.wrapper.TrackerLog +import me.rhunk.snapenhance.common.data.TrackerEventType +import me.rhunk.snapenhance.common.data.TrackerRule +import me.rhunk.snapenhance.common.data.TrackerRuleEvent +import me.rhunk.snapenhance.ui.manager.Routes +import me.rhunk.snapenhance.ui.util.pagerTabIndicatorOffset +import java.text.DateFormat + + +@OptIn(ExperimentalFoundationApi::class) +class FriendTrackerManagerRoot : Routes.Route() { + enum class FilterType { + CONVERSATION, USERNAME, EVENT + } + + private val titles = listOf("Logs", "Config Rules") + private var currentPage by mutableIntStateOf(0) + + override val floatingActionButton: @Composable () -> Unit = { + if (currentPage == 1) { + ExtendedFloatingActionButton( + icon = { Icon(Icons.Default.Add, contentDescription = "Add Rule") }, + expanded = false, + text = {}, + onClick = {} + ) + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun LogsTab() { + val coroutineScope = rememberCoroutineScope() + + val logs = remember { mutableStateListOf() } + var lastTimestamp by remember { mutableLongStateOf(Long.MAX_VALUE) } + var filterType by remember { mutableStateOf(FilterType.USERNAME) } + + var filter by remember { mutableStateOf("") } + var searchTimeoutJob by remember { mutableStateOf(null) } + + suspend fun loadNewLogs() { + withContext(Dispatchers.IO) { + logs.addAll(context.messageLogger.getLogs(lastTimestamp, filter = { + when (filterType) { + FilterType.USERNAME -> it.username.contains(filter, ignoreCase = true) + FilterType.CONVERSATION -> it.conversationTitle?.contains(filter, ignoreCase = true) == true || (it.username == filter && !it.isGroup) + FilterType.EVENT -> it.eventType.contains(filter, ignoreCase = true) + } + }).apply { + lastTimestamp = minOfOrNull { it.timestamp } ?: lastTimestamp + }) + } + } + + suspend fun resetAndLoadLogs() { + logs.clear() + lastTimestamp = Long.MAX_VALUE + loadNewLogs() + } + + Column( + modifier = Modifier.fillMaxSize() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + var showAutoComplete by remember { mutableStateOf(false) } + ExposedDropdownMenuBox(expanded = showAutoComplete, onExpandedChange = { showAutoComplete = it }) { + TextField( + value = filter, + onValueChange = { + filter = it + coroutineScope.launch { + searchTimeoutJob?.cancel() + searchTimeoutJob = coroutineScope.launch { + delay(200) + showAutoComplete = true + resetAndLoadLogs() + } + } + }, + placeholder = { Text("Search") }, + maxLines = 1, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent + ), + modifier = Modifier + .weight(1F) + .menuAnchor() + .padding(8.dp) + ) + + DropdownMenu(expanded = showAutoComplete, onDismissRequest = { + showAutoComplete = false + }, properties = PopupProperties(focusable = false)) { + val suggestedEntries = remember(filter) { + mutableStateListOf() + } + + LaunchedEffect(filter) { + suggestedEntries.addAll(when (filterType) { + FilterType.USERNAME -> context.messageLogger.findUsername(filter) + FilterType.CONVERSATION -> context.messageLogger.findConversation(filter) + context.messageLogger.findUsername(filter) + FilterType.EVENT -> TrackerEventType.entries.filter { it.name.contains(filter, ignoreCase = true) }.map { it.key } + }.take(5)) + } + + suggestedEntries.forEach { entry -> + DropdownMenuItem(onClick = { + filter = entry + coroutineScope.launch { + resetAndLoadLogs() + } + showAutoComplete = false + }, text = { + Text(entry) + }) + } + } + } + + var dropDownExpanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox(expanded = dropDownExpanded, onExpandedChange = { dropDownExpanded = it }) { + ElevatedCard( + modifier = Modifier.menuAnchor() + ) { + Text("Filter " + filterType.name, modifier = Modifier.padding(8.dp)) + } + DropdownMenu(expanded = dropDownExpanded, onDismissRequest = { + dropDownExpanded = false + }) { + FilterType.entries.forEach { type -> + DropdownMenuItem(onClick = { + filterType = type + dropDownExpanded = false + coroutineScope.launch { + resetAndLoadLogs() + } + }, text = { + Text(type.name) + }) + } + } + } + } + + LazyColumn( + modifier = Modifier.weight(1f) + ) { + item { + if (logs.isEmpty()) { + Text("No logs found", modifier = Modifier.padding(16.dp).fillMaxWidth(), textAlign = TextAlign.Center, fontWeight = FontWeight.Light) + } + } + items(logs) { log -> + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + Column( + modifier = Modifier + .weight(1f) + ) { + Text(log.username + " " + log.eventType + " in " + log.conversationTitle) + Text( + DateFormat.getDateTimeInstance().format(log.timestamp), + fontSize = 12.sp, + fontWeight = FontWeight.Light + ) + } + + OutlinedIconButton( + onClick = { + + } + ) { + Icon(Icons.Default.DeleteOutline, contentDescription = "Delete") + } + } + } + } + item { + Spacer(modifier = Modifier.height(16.dp)) + + LaunchedEffect(lastTimestamp) { + loadNewLogs() + } + } + } + } + + } + + @Composable + @OptIn(ExperimentalLayoutApi::class) + private fun ConfigRulesTab() { + val rules = remember { mutableStateListOf() } + + Column( + modifier = Modifier.fillMaxSize() + ) { + LazyColumn( + modifier = Modifier.weight(1f) + ) { + items(rules) { rule -> + val events = remember(rule.id) { + mutableStateListOf() + } + + LaunchedEffect(rule.id) { + withContext(Dispatchers.IO) { + events.addAll(context.modDatabase.getTrackerEvents(rule.id)) + } + } + + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text("Rule: ${rule.id} - conversationId: ${rule.conversationId?.let { "present" } ?: "none" } - userId: ${rule.userId?.let { "present" } ?: "none"}") + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + events.forEach { event -> + Text("${event.eventType} - ${event.flags}") + } + } + } + } + } + } + } + + LaunchedEffect(Unit) { + rules.addAll(context.modDatabase.getTrackerRules(null, null)) + } + } + + + @OptIn(ExperimentalFoundationApi::class) + override val content: @Composable (NavBackStackEntry) -> Unit = { + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState { titles.size } + currentPage = pagerState.currentPage + + Column { + TabRow(selectedTabIndex = pagerState.currentPage, indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.pagerTabIndicatorOffset( + pagerState = pagerState, + tabPositions = tabPositions + ) + ) + }) { + titles.forEachIndexed { index, title -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { + Text( + text = title, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + ) + } + } + + HorizontalPager( + modifier = Modifier.weight(1f), + state = pagerState + ) { page -> + when (page) { + 0 -> LogsTab() + 1 -> ConfigRulesTab() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt index 84f7de2b..778fcb4f 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/LoggerHistoryRoot.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.common.bridge.wrapper.LoggedMessage -import me.rhunk.snapenhance.common.bridge.wrapper.MessageLoggerWrapper +import me.rhunk.snapenhance.common.bridge.wrapper.LoggerWrapper import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.download.* import me.rhunk.snapenhance.common.util.ktx.copyToClipboard @@ -40,7 +40,7 @@ import kotlin.math.absoluteValue class LoggerHistoryRoot : Routes.Route() { - private lateinit var messageLoggerWrapper: MessageLoggerWrapper + private lateinit var loggerWrapper: LoggerWrapper private var selectedConversation by mutableStateOf(null) private var stringFilter by mutableStateOf("") private var reverseOrder by mutableStateOf(true) @@ -182,7 +182,7 @@ class LoggerHistoryRoot : Routes.Route() { @OptIn(ExperimentalMaterial3Api::class) override val content: @Composable (NavBackStackEntry) -> Unit = { LaunchedEffect(Unit) { - messageLoggerWrapper = MessageLoggerWrapper( + loggerWrapper = LoggerWrapper( context.androidContext.getDatabasePath("message_logger.db") ) } @@ -208,7 +208,7 @@ class LoggerHistoryRoot : Routes.Route() { LaunchedEffect(Unit) { conversations.clear() withContext(Dispatchers.IO) { - conversations.addAll(messageLoggerWrapper.getAllConversations()) + conversations.addAll(loggerWrapper.getAllConversations()) } } @@ -270,7 +270,7 @@ class LoggerHistoryRoot : Routes.Route() { } LaunchedEffect(Unit, selectedConversation, stringFilter, reverseOrder) { withContext(Dispatchers.IO) { - val newMessages = messageLoggerWrapper.fetchMessages( + val newMessages = loggerWrapper.fetchMessages( selectedConversation ?: return@withContext, lastFetchMessageTimestamp, 30, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeRoot.kt index 9102fba9..9c2f24ca 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeRoot.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeRoot.kt @@ -271,6 +271,18 @@ class HomeRoot : Routes.Route() { updateInstallationSummary(coroutineScope) } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + ElevatedButton(onClick = { + routes.friendTracker.navigate() + }) { + Text(text = "Friend Tracker", fontSize = 18.sp) + } + } + + Spacer(modifier = Modifier.height(20.dp)) installationSummary?.let { SummaryCards(installationSummary = it) } } } diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl index a11eabf1..10bca920 100644 --- a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl +++ b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -5,7 +5,8 @@ import me.rhunk.snapenhance.bridge.DownloadCallback; import me.rhunk.snapenhance.bridge.SyncCallback; import me.rhunk.snapenhance.bridge.scripting.IScripting; import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface; -import me.rhunk.snapenhance.bridge.MessageLoggerInterface; +import me.rhunk.snapenhance.bridge.logger.LoggerInterface; +import me.rhunk.snapenhance.bridge.logger.TrackerInterface; import me.rhunk.snapenhance.bridge.ConfigStateListener; import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge; @@ -79,7 +80,9 @@ interface BridgeInterface { E2eeInterface getE2eeInterface(); - MessageLoggerInterface getMessageLogger(); + LoggerInterface getLogger(); + + TrackerInterface getTracker(); oneway void registerMessagingBridge(MessagingBridge bridge); diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/MessageLoggerInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggerInterface.aidl similarity index 73% rename from common/src/main/aidl/me/rhunk/snapenhance/bridge/MessageLoggerInterface.aidl rename to common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggerInterface.aidl index 2f1a45ce..320071a8 100644 --- a/common/src/main/aidl/me/rhunk/snapenhance/bridge/MessageLoggerInterface.aidl +++ b/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/LoggerInterface.aidl @@ -1,6 +1,6 @@ -package me.rhunk.snapenhance.bridge; +package me.rhunk.snapenhance.bridge.logger; -interface MessageLoggerInterface { +interface LoggerInterface { /** * Get the ids of the messages that are logged * @return message ids that are logged @@ -26,4 +26,14 @@ interface MessageLoggerInterface { * Add a story to the message logger database if it is not already there */ boolean addStory(String userId, String url, long postedAt, long createdAt, in byte[] key, in byte[] iv); + + oneway void logTrackerEvent( + String conversationId, + String conversationTitle, + boolean isGroup, + String username, + String userId, + String eventType, + String data + ); } \ No newline at end of file diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/TrackerInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/TrackerInterface.aidl new file mode 100644 index 00000000..bd843802 --- /dev/null +++ b/common/src/main/aidl/me/rhunk/snapenhance/bridge/logger/TrackerInterface.aidl @@ -0,0 +1,5 @@ +package me.rhunk.snapenhance.bridge.logger; + +interface TrackerInterface { + String getTrackedEvents(String eventType); // returns serialized TrackerEventsResult +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MessageLoggerWrapper.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt similarity index 69% rename from common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MessageLoggerWrapper.kt rename to common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt index 3cd82bcd..4cb0b504 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/MessageLoggerWrapper.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/wrapper/LoggerWrapper.kt @@ -3,10 +3,11 @@ package me.rhunk.snapenhance.common.bridge.wrapper import android.content.ContentValues import android.database.sqlite.SQLiteDatabase import kotlinx.coroutines.* -import me.rhunk.snapenhance.bridge.MessageLoggerInterface +import me.rhunk.snapenhance.bridge.logger.LoggerInterface import me.rhunk.snapenhance.common.data.StoryData import me.rhunk.snapenhance.common.util.SQLiteDatabaseHelper import me.rhunk.snapenhance.common.util.ktx.getBlobOrNull +import me.rhunk.snapenhance.common.util.ktx.getIntOrNull import me.rhunk.snapenhance.common.util.ktx.getLongOrNull import me.rhunk.snapenhance.common.util.ktx.getStringOrNull import java.io.File @@ -19,9 +20,20 @@ class LoggedMessage( val messageData: ByteArray ) -class MessageLoggerWrapper( +class TrackerLog( + val timestamp: Long, + val conversationId: String, + val conversationTitle: String?, + val isGroup: Boolean, + val username: String, + val userId: String, + val eventType: String, + val data: String +) + +class LoggerWrapper( val databaseFile: File -): MessageLoggerInterface.Stub() { +): LoggerInterface.Stub() { private var _database: SQLiteDatabase? = null @OptIn(ExperimentalCoroutinesApi::class) private val coroutineScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1)) @@ -47,6 +59,17 @@ class MessageLoggerWrapper( "url VARCHAR", "encryption_key BLOB", "encryption_iv BLOB" + ), + "tracker_events" to listOf( + "id INTEGER PRIMARY KEY", + "timestamp BIGINT", + "conversation_id CHAR(36)", + "conversation_title VARCHAR", + "is_group BOOLEAN", + "username VARCHAR", + "user_id VARCHAR", + "event_type VARCHAR", + "data VARCHAR" ) )) _database = openedDatabase @@ -159,6 +182,76 @@ class MessageLoggerWrapper( return true } + override fun logTrackerEvent( + conversationId: String, + conversationTitle: String?, + isGroup: Boolean, + username: String, + userId: String, + eventType: String, + data: String + ) { + runBlocking { + withContext(coroutineScope.coroutineContext) { + database.insert("tracker_events", null, ContentValues().apply { + put("timestamp", System.currentTimeMillis()) + put("conversation_id", conversationId) + put("conversation_title", conversationTitle) + put("is_group", isGroup) + put("username", username) + put("user_id", userId) + put("event_type", eventType) + put("data", data) + }) + } + } + } + + fun getLogs( + lastTimestamp: Long, + filter: ((TrackerLog) -> Boolean)? = null + ): List { + return database.rawQuery("SELECT * FROM tracker_events WHERE timestamp < ? ORDER BY timestamp DESC", arrayOf(lastTimestamp.toString())).use { + val logs = mutableListOf() + while (it.moveToNext() && logs.size < 50) { + val log = TrackerLog( + timestamp = it.getLongOrNull("timestamp") ?: continue, + conversationId = it.getStringOrNull("conversation_id") ?: continue, + conversationTitle = it.getStringOrNull("conversation_title"), + isGroup = it.getIntOrNull("is_group") == 1, + username = it.getStringOrNull("username") ?: continue, + userId = it.getStringOrNull("user_id") ?: continue, + eventType = it.getStringOrNull("event_type") ?: continue, + data = it.getStringOrNull("data") ?: continue + ) + if (filter != null && !filter(log)) continue + logs.add(log) + } + logs + } + } + + fun findConversation(search: String): List { + return database.rawQuery("SELECT DISTINCT conversation_id FROM tracker_events WHERE is_group = 1 AND conversation_id LIKE ?", arrayOf("%$search%")).use { + val conversations = mutableListOf() + while (it.moveToNext()) { + conversations.add(it.getString(0)) + } + conversations + } + } + + fun findUsername(search: String): List { + return database.rawQuery("SELECT DISTINCT username FROM tracker_events WHERE username LIKE ?", arrayOf("%$search%")).use { + val usernames = mutableListOf() + while (it.moveToNext()) { + usernames.add(it.getString(0)) + } + usernames + } + } + + fun getStories(userId: String, from: Long, limit: Int = Int.MAX_VALUE): Map { val stories = sortedMapOf() database.rawQuery("SELECT * FROM stories WHERE user_id = ? AND posted_timestamp < ? ORDER BY posted_timestamp DESC LIMIT $limit", arrayOf(userId, from.toString())).use { diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SessionEventsData.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SessionEventsData.kt index 3a8c97c2..324df0d1 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SessionEventsData.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SessionEventsData.kt @@ -1,5 +1,8 @@ package me.rhunk.snapenhance.common.data +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + data class FriendPresenceState( val bitmojiPresent: Boolean, @@ -40,3 +43,94 @@ enum class SessionEventType( SNAP_SCREENSHOT("snap_screenshot"), SNAP_SCREEN_RECORD("snap_screen_record"), } + +object TrackerFlags { + const val TRACK = 1 + const val LOG = 2 + const val NOTIFY = 4 + const val APP_IS_ACTIVE = 8 + const val APP_IS_INACTIVE = 16 + const val IS_IN_CONVERSATION = 32 +} + +@Parcelize +class TrackerEventsResult( + private val rules: Map> +): Parcelable { + fun hasFlags(vararg flags: Int): Boolean { + return rules.any { (_, ruleEvents) -> + ruleEvents.any { flags.all { flag -> it.flags and flag != 0 } } + } + } + + fun canTrackOn(conversationId: String?, userId: String?): Boolean { + return rules.any t@{ (rule, ruleEvents) -> + ruleEvents.any { event -> + if (event.flags and TrackerFlags.TRACK == 0) { + return@any false + } + + // global rule + if (rule.conversationId == null && rule.userId == null) { + return@any true + } + + // user rule + if (rule.conversationId == null && rule.userId == userId) { + return@any true + } + + // conversation rule + if (rule.conversationId == conversationId && rule.userId == null) { + return@any true + } + + // conversation and user rule + return@any rule.conversationId == conversationId && rule.userId == userId + } + } + } +} + + +@Parcelize +data class TrackerRule( + val id: Int, + val flags: Int, + val conversationId: String?, + val userId: String? +): Parcelable + +@Parcelize +data class TrackerRuleEvent( + val id: Int, + val flags: Int, + val eventType: String, +): Parcelable + +enum class TrackerEventType( + val key: String +) { + // pcs events + CONVERSATION_ENTER("conversation_enter"), + CONVERSATION_EXIT("conversation_exit"), + STARTED_TYPING("started_typing"), + STOPPED_TYPING("stopped_typing"), + STARTED_SPEAKING("started_speaking"), + STOPPED_SPEAKING("stopped_speaking"), + STARTED_PEEKING("started_peeking"), + STOPPED_PEEKING("stopped_peeking"), + + // mcs events + MESSAGE_READ("message_read"), + 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"), +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt index 267ce8f3..1de4f0d4 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -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) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt index fc4e5f5f..582c116a 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt @@ -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( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SessionEvents.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SessionEvents.kt index dcd9f4b5..7cbfbc9b 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SessionEvents.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SessionEvents.kt @@ -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>() // 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(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)) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt index 8e3adb28..4ae80811 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/MessageLogger.kt @@ -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