feat(experimental): best friend pinning

This commit is contained in:
rhunk 2024-04-28 18:06:02 +02:00
parent ddf1edb35d
commit 174dca6754
8 changed files with 168 additions and 1 deletions

View File

@ -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!",

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -127,6 +127,7 @@ class FeatureManager(
CustomStreaksExpirationFormat(),
ComposerHooks(),
DisableCustomTabs(),
BestFriendPinning(),
)
initializeFeatures()
}

View File

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