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

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

View File

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

View File

@@ -0,0 +1,5 @@
package me.rhunk.snapenhance.bridge.logger;
interface TrackerInterface {
String getTrackedEvents(String eventType); // returns serialized TrackerEventsResult
}

View File

@@ -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<TrackerLog> {
return database.rawQuery("SELECT * FROM tracker_events WHERE timestamp < ? ORDER BY timestamp DESC", arrayOf(lastTimestamp.toString())).use {
val logs = mutableListOf<TrackerLog>()
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<String> {
return database.rawQuery("SELECT DISTINCT conversation_id FROM tracker_events WHERE is_group = 1 AND conversation_id LIKE ?", arrayOf("%$search%")).use {
val conversations = mutableListOf<String>()
while (it.moveToNext()) {
conversations.add(it.getString(0))
}
conversations
}
}
fun findUsername(search: String): List<String> {
return database.rawQuery("SELECT DISTINCT username FROM tracker_events WHERE username LIKE ?", arrayOf("%$search%")).use {
val usernames = mutableListOf<String>()
while (it.moveToNext()) {
usernames.add(it.getString(0))
}
usernames
}
}
fun getStories(userId: String, from: Long, limit: Int = Int.MAX_VALUE): Map<Long, StoryData> {
val stories = sortedMapOf<Long, StoryData>()
database.rawQuery("SELECT * FROM stories WHERE user_id = ? AND posted_timestamp < ? ORDER BY posted_timestamp DESC LIMIT $limit", arrayOf(userId, from.toString())).use {

View File

@@ -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<TrackerRule, List<TrackerRuleEvent>>
): 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"),
}