commit 174dca6754c27b100065674f10949b944a8c29b2
parent ddf1edb35dcc216cd1377300c097470796f6e3ba
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Sun, 28 Apr 2024 18:06:02 +0200
feat(experimental): best friend pinning
Diffstat:
8 files changed, 169 insertions(+), 1 deletion(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -936,6 +936,10 @@
"name": "No Friend Score Delay",
"description": "Removes the delay when viewing a Friends Score"
},
+ "best_friend_pinning": {
+ "name": "Best Friend Pinning",
+ "description": "Allows you to pin a friend as your number one best friend. Note: only you can see your pinned best friend"
+ },
"e2ee": {
"name": "End-To-End Encryption",
"description": "Encrypts your messages with AES using a shared secret key\nMake sure to save your key somewhere safe!",
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt
@@ -9,7 +9,8 @@ enum class BridgeFileType(val value: Int, val fileName: String, val displayName:
MAPPINGS(1, "mappings.json", "Mappings"),
MESSAGE_LOGGER_DATABASE(2, "message_logger.db", "Message Logger",true),
PINNED_CONVERSATIONS(3, "pinned_conversations.txt", "Pinned Conversations"),
- SUSPEND_LOCATION_STATE(4, "suspend_location_state.txt", "Suspend Location State");
+ SUSPEND_LOCATION_STATE(4, "suspend_location_state.txt", "Suspend Location State"),
+ PINNED_BEST_FRIEND(5, "pinned_best_friend.txt", "Pinned Best Friend");
fun resolve(context: Context): File = if (isDatabase) {
context.getDatabasePath(fileName)
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
@@ -48,6 +48,7 @@ class Experimental : ConfigContainer() {
val infiniteStoryBoost = boolean("infinite_story_boost")
val meoPasscodeBypass = boolean("meo_passcode_bypass")
val noFriendScoreDelay = boolean("no_friend_score_delay") { requireRestart()}
+ val bestFriendPinning = boolean("best_friend_pinning") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val e2eEncryption = container("e2ee", E2EEConfig()) { requireRestart(); nativeHooks() }
val hiddenSnapchatPlusFeatures = boolean("hidden_snapchat_plus_features") {
addNotices(FeatureNotice.BAN_RISK, FeatureNotice.UNSTABLE)
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/ProtoReader.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/ProtoReader.kt
@@ -167,6 +167,7 @@ class ProtoReader(private val buffer: ByteArray) {
}
return value
}
+ fun getFixed64(vararg ids: Int) = followPath(*ids, excludeLast = true)?.getFixed64(ids.last())
fun getFixed32(id: Int): Int {
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
@@ -429,4 +429,62 @@ class DatabaseAccess(
}
}
}
+
+ private fun getBestFriends(): List<FriendInfo> {
+ return useDatabase(DatabaseType.MAIN)?.performOperation {
+ safeRawQuery(
+ "SELECT * FROM Friend WHERE friendmojiCategories != ''",
+ null
+ )?.use { query ->
+ val list = mutableListOf<FriendInfo>()
+ while (query.moveToNext()) {
+ val friendInfo = FriendInfo()
+ try {
+ friendInfo.write(query)
+ } catch (_: Throwable) {}
+ list.add(friendInfo)
+ }
+ list
+ }
+ } ?: emptyList()
+ }
+
+ fun updatePinnedBestFriendStatus(userId: String, friendmoji: String) {
+ useDatabase(DatabaseType.MAIN, writeMode = true)?.apply {
+ val numberOneBestFriends = getBestFriends().filter { friend ->
+ friend.friendmojiCategories?.split(",")?.any { it.startsWith("number_one") } == true
+ }
+
+ numberOneBestFriends.forEach { friendInfo ->
+ performOperation {
+ update(
+ "Friend",
+ ContentValues().apply {
+ put("friendmojiCategories", friendInfo.friendmojiCategories?.split(",")?.filter {
+ it == "on_fire" || it == "birthday"
+ }?.joinToString(",") ?: "")
+ put("isPinnedBestFriend", 0)
+ },
+ "userId = ?",
+ arrayOf(friendInfo.userId)
+ )
+ }
+ }
+
+ val friend = getFriendInfo(userId) ?: return@apply
+ performOperation {
+ update(
+ "Friend",
+ ContentValues().apply {
+ put("friendmojiCategories", (friend.friendmojiCategories?.split(",") ?: listOf()).toMutableList().apply {
+ add(friendmoji)
+ }.joinToString(","))
+ put("isPinnedBestFriend", 1)
+ },
+ "userId = ?",
+ arrayOf(userId)
+ )
+ }
+ }?.close()
+ }
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt
@@ -51,4 +51,11 @@ abstract class BridgeFileFeature(name: String, private val bridgeFileType: Bridg
fileLines.add(line)
updateFile()
}
+
+ protected fun clear() {
+ fileLines.clear()
+ updateFile()
+ }
+
+ protected fun lines() = fileLines.toList()
}
\ 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
@@ -127,6 +127,7 @@ class FeatureManager(
CustomStreaksExpirationFormat(),
ComposerHooks(),
DisableCustomTabs(),
+ BestFriendPinning(),
)
initializeFeatures()
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt
@@ -0,0 +1,94 @@
+package me.rhunk.snapenhance.core.features.impl.experiments
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.FavoriteBorder
+import com.google.gson.JsonArray
+import com.google.gson.JsonObject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
+import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
+import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent
+import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
+import me.rhunk.snapenhance.core.features.BridgeFileFeature
+import me.rhunk.snapenhance.core.features.FeatureLoadParams
+import me.rhunk.snapenhance.core.ui.triggerRootCloseTouchEvent
+import java.io.InputStreamReader
+import java.nio.ByteBuffer
+import java.util.UUID
+
+class BestFriendPinning: BridgeFileFeature("Best Friend Pinning", BridgeFileType.PINNED_BEST_FRIEND, loadParams = FeatureLoadParams.INIT_SYNC) {
+ private fun updatePinnedBestFriendStatus() {
+ lines().firstOrNull()?.trim()?.let {
+ context.database.updatePinnedBestFriendStatus(it.substring(0, 36), "number_one_bf_for_two_months")
+ }
+ }
+
+ override fun init() {
+ if (!context.config.experimental.bestFriendPinning.get()) return
+ reload()
+
+ context.event.subscribe(UnaryCallEvent::class) { event ->
+ if (!event.uri.endsWith("/PinBestFriend") && !event.uri.endsWith("/UnpinBestFriend")) return@subscribe
+ event.canceled = true
+ val userId = ProtoReader(event.buffer).let {
+ UUID(it.getFixed64(1, 1) ?: return@subscribe, it.getFixed64(1, 2)?: return@subscribe).toString()
+ }
+
+ clear()
+ put(userId)
+
+ updatePinnedBestFriendStatus()
+
+ val username = context.database.getFriendInfo(userId)?.mutableUsername ?: "Unknown"
+
+ context.inAppOverlay.showStatusToast(
+ icon = Icons.Default.FavoriteBorder,
+ "Pinned $username as best friend! Please restart the app to apply changes.",
+ durationMs = 5000
+ )
+
+ context.coroutineScope.launch(Dispatchers.Main) {
+ delay(500)
+ @Suppress("DEPRECATION")
+ context.mainActivity!!.onBackPressed()
+ context.mainActivity!!.triggerRootCloseTouchEvent()
+ }
+ }
+
+ context.event.subscribe(NetworkApiRequestEvent::class) { event ->
+ if (!event.url.contains("ami/friends")) return@subscribe
+ val pinnedBFF = lines().firstOrNull()?.trim() ?: return@subscribe
+
+ event.onSuccess { buffer ->
+ val jsonObject = context.gson.fromJson(
+ InputStreamReader(buffer?.inputStream() ?: return@onSuccess, Charsets.UTF_8),
+ JsonObject::class.java
+ ).apply {
+ getAsJsonArray("friends").map { it.asJsonObject }.forEach { friend ->
+ if (friend.get("user_id").asString != pinnedBFF) return@forEach
+ friend.add("friendmojis", JsonArray().apply {
+ friend.getAsJsonArray("friendmojis").map { it.asJsonObject }.forEach { friendmoji ->
+ val category = friendmoji.get("category_name").asString
+ if (category == "on_fire" || category == "birthday") {
+ add(friendmoji)
+ }
+ }
+ add(JsonObject().apply {
+ addProperty("category_name", "number_one_bf_for_two_months")
+ })
+ })
+ }
+ }
+
+ jsonObject.toString().toByteArray(Charsets.UTF_8).let {
+ setArg(2, ByteBuffer.allocateDirect(it.size).apply {
+ put(it)
+ flip()
+ })
+ }
+ }
+ }
+ }
+}+
\ No newline at end of file