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:
Mcommon/src/main/assets/lang/en_US.json | 4++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/types/BridgeFileType.kt | 3++-
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt | 1+
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/util/protobuf/ProtoReader.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/BridgeFileFeature.kt | 7+++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt | 1+
Acore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/BestFriendPinning.kt | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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