commit c7a2d0268796638661c54f724c8f08f1a2676a9d
parent 7ded784d8c850fbf98d59bd3fb5bb13b073a6c64
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sat,  8 Mar 2025 12:31:18 +0100
feat(core): snap score changes
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
Diffstat:
8 files changed, 109 insertions(+), 0 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteTracker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteTracker.kt
@@ -8,6 +8,7 @@ import me.rhunk.snapenhance.common.data.TrackerRuleEvent
 import me.rhunk.snapenhance.common.util.toSerialized
 import me.rhunk.snapenhance.storage.getRuleTrackerScopes
 import me.rhunk.snapenhance.storage.getTrackerEvents
+import me.rhunk.snapenhance.storage.updateFriendScore
 
 
 class RemoteTracker(
@@ -26,4 +27,8 @@ class RemoteTracker(
             ScopedTrackerRule(it.key, context.database.getRuleTrackerScopes(it.key.id))
         }).toSerialized()
     }
+
+    override fun updateFriendScore(userId: String, score: Long): Long {
+        return context.database.updateFriendScore(userId, score)
+    }
 } 
\ No newline at end of file
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt
@@ -78,6 +78,10 @@ class AppDatabase(
                 "params TEXT",
                 "actions TEXT"
             ),
+            "friend_scores" to listOf(
+                "userId CHAR(36) PRIMARY KEY",
+                "score BIGINT"
+            ),
             "quick_tiles" to listOf(
                 "key VARCHAR PRIMARY KEY",
                 "position INTEGER",
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/Tracker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/Tracker.kt
@@ -9,6 +9,7 @@ import me.rhunk.snapenhance.common.data.TrackerRuleActionParams
 import me.rhunk.snapenhance.common.data.TrackerRuleEvent
 import me.rhunk.snapenhance.common.data.TrackerScopeType
 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 kotlin.coroutines.suspendCoroutine
 
@@ -195,3 +196,24 @@ fun AppDatabase.getRuleTrackerScopes(ruleId: Int, limit: Int = Int.MAX_VALUE): M
     }
     return scopes
 }
+
+fun AppDatabase.updateFriendScore(userId: String, score: Long): Long {
+    return runBlocking {
+        suspendCoroutine { continuation ->
+            executeAsync {
+                val currentScore = database.rawQuery("SELECT score FROM friend_scores WHERE userId = ?", arrayOf(userId)).use { cursor ->
+                    if (!cursor.moveToFirst()) return@use null
+                    cursor.getLongOrNull("score")
+                }
+
+                if (currentScore != null) {
+                    database.execSQL("UPDATE friend_scores SET score = ? WHERE userId = ?", arrayOf(score, userId))
+                } else {
+                    database.execSQL("INSERT INTO friend_scores (userId, score) VALUES (?, ?)", arrayOf(userId, score))
+                }
+
+                continuation.resumeWith(Result.success(currentScore ?: -1))
+            }
+        }
+    }
+}+
\ 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
@@ -2,4 +2,6 @@ package me.rhunk.snapenhance.bridge.logger;
 
 interface TrackerInterface {
     String getTrackedEvents(String eventType); // returns serialized TrackerEventsResult
+
+    long updateFriendScore(String userId, long score); // returns old score (-1 if not found)
 } 
\ No newline at end of file
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -1152,6 +1152,10 @@
                     "prevent_forced_logout": {
                         "name": "Prevent Forced Logout",
                         "description": "Prevents Snapchat from logging you out when you login on another device"
+                    },
+                    "snapscore_changes": {
+                        "name": "Snapscore Changes",
+                        "description": "Tracks changes in friends Snapscore\nUse this feature in newer versions of Snapchat only"
                     }
                 }
             },
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt
@@ -94,4 +94,5 @@ class Experimental : ConfigContainer() {
         "added_by_quick_add",
     ) { addNotices(FeatureNotice.BAN_RISK) }
     val preventForcedLogout = boolean("prevent_forced_logout") { requireRestart(); addNotices(FeatureNotice.BAN_RISK, FeatureNotice.INTERNAL_BEHAVIOR); }
+    val snapScoreChanges = boolean("snapscore_changes") { requireRestart() }
 } 
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt
@@ -142,6 +142,7 @@ class FeatureManager(
             VoiceNoteOverride(),
             FriendNotes(),
             DoubleTapChatAction(),
+            SnapScoreChanges(),
         )
 
         features.values.toList().forEach { feature ->
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SnapScoreChanges.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/SnapScoreChanges.kt
@@ -0,0 +1,68 @@
+package me.rhunk.snapenhance.core.features.impl.experiments
+
+import android.view.ViewGroup
+import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
+import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
+import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
+import me.rhunk.snapenhance.core.features.Feature
+import me.rhunk.snapenhance.core.ui.getComposerContext
+import me.rhunk.snapenhance.core.ui.getComposerViewNode
+import me.rhunk.snapenhance.core.util.ktx.getObjectField
+import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
+
+class SnapScoreChanges: Feature("Snap Score Changes") {
+    private val scores = mutableMapOf<String, Long>()
+    private var lastViewedUserId: String? = null
+
+    override fun init() {
+        if (!context.config.experimental.snapScoreChanges.get()) return
+
+        context.event.subscribe(UnaryCallEvent::class) { event ->
+            if (event.uri != "/com.snapchat.atlas.gw.AtlasGw/GetFriendsUserScore") return@subscribe
+
+            event.addResponseCallback {
+                synchronized(scores) {
+                    ProtoReader(buffer).eachBuffer(1) {
+                        val friendUUID = getByteArray(1) ?: return@eachBuffer
+                        val score = getVarInt(2) ?: return@eachBuffer
+
+                        scores[SnapUUID(friendUUID).toString()] = score
+                    }
+                }
+            }
+        }
+
+        context.event.subscribe(AddViewEvent::class) { event ->
+            if (event.viewClassName.endsWith("UnifiedProfileFlatlandProfileViewTopViewFrameLayout")) {
+                val composerView = (event.view as ViewGroup).getChildAt(0) ?: return@subscribe
+                val composerContext = composerView.getComposerContext() ?: return@subscribe
+
+                lastViewedUserId = composerContext.viewModel?.getObjectField("_userId")?.toString()
+            }
+
+            if (event.viewClassName.endsWith("ProfileFlatlandFriendSnapScoreIdentityPillDialogView")) {
+                event.view.post {
+                    val composerViewNode = event.view.getComposerViewNode() ?: return@post
+                    val surface = composerViewNode.getChildren().getOrNull(1) ?: return@post
+
+                    val snapTextView = surface.getChildren().lastOrNull {
+                        it.getClassName() == "com.snap.composer.views.ComposerSnapTextView"
+                    } ?: return@post
+
+
+                    val currentFriendScore = scores[lastViewedUserId] ?: (event.view.getComposerContext()?.viewModel?.getObjectField("_friendSnapScore") as? Double)?.toLong() ?: return@post
+
+                    val oldSnapScore = context.bridgeClient.getTracker().updateFriendScore(
+                        lastViewedUserId ?: return@post,
+                        currentFriendScore
+                    )
+
+                    val diff = currentFriendScore - oldSnapScore
+
+                    snapTextView.setAttribute("value", "${if (oldSnapScore != -1L && diff > 0) "\uD83D\uDCC8 +$diff !\n\n" else ""}Last Checked Score: ${oldSnapScore.takeIf { it != -1L } ?: "N/A"}")
+                    event.view.invalidate()
+                }
+            }
+        }
+    }
+}+
\ No newline at end of file