multiple fixes

This commit is contained in:
particle-box
2026-01-23 00:34:27 +05:30
parent 18f280cfd7
commit 2c5f54368c
26 changed files with 1160 additions and 758 deletions

View File

@@ -4,40 +4,36 @@ import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -48,30 +44,17 @@ import me.eternal.purrfectsnap.common.data.FriendLinkType
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
import me.eternal.purrfectsnap.core.action.AbstractAction
import me.eternal.purrfectsnap.core.event.events.impl.ActivityResultEvent
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
import me.eternal.purrfectsnap.common.util.snap.RemoteMediaResolver
import me.eternal.purrfectsnap.core.features.impl.experiments.AddFriendSourceSpoof
import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging
import me.eternal.purrfectsnap.core.util.EvictingMap
import me.eternal.purrfectsnap.core.wrapper.impl.Snapchatter
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
import me.eternal.purrfectsnap.common.util.snap.RemoteMediaResolver
import kotlin.random.Random
import java.text.SimpleDateFormat
import java.util.*
class ManageFriendList : AbstractAction() {
companion object {
private var openSuggestedOnLaunch = false
@Synchronized
fun requestOpenSuggestedOnLaunch() {
openSuggestedOnLaunch = true
}
@Synchronized
private fun consumeOpenSuggestedOnLaunch(): Boolean {
val shouldOpen = openSuggestedOnLaunch
openSuggestedOnLaunch = false
return shouldOpen
}
}
private val translation by lazy { context.translation.getCategory("friend_list") }
private val dialogBackground = Brush.verticalGradient(
listOf(
@@ -92,211 +75,7 @@ class ManageFriendList : AbstractAction() {
)
)
private var pendingPickerAction: Pair<Int, (data: Uri) -> Unit>? = null
private val uuidRegex by lazy {
Regex("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")
}
private fun loadStaticDefaultField(classLoader: ClassLoader, className: String, fieldName: String): Any? {
return try {
val clazz = classLoader.loadClass(className)
try {
val field = clazz.getDeclaredField(fieldName)
field.isAccessible = true
field.get(null)
} catch (e: Exception) {
clazz.declaredFields
.filter { java.lang.reflect.Modifier.isStatic(it.modifiers) }
.firstOrNull()
?.apply { isAccessible = true }
?.get(null)
}
} catch (e: Exception) {
context.log.warn("Could not load $className: ${e.message}")
null
}
}
private fun addFriend(userId: String) {
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance
if (friendRelationshipChangerInstance == null) {
context.log.error("friendRelationshipChangerInstance is null")
context.longToast("Failed to add friend: FriendRelationshipChanger instance not available")
return
}
runCatching {
val classLoader = context.androidContext.classLoader
val classNamesToTry = listOf("EnumC3559qC", "qC", "com.snapchat.android.EnumC3559qC", "LC", "EnumC0886LC", "EnumC1539TC", "TC")
val enumC3559qCClass = classNamesToTry.firstNotNullOfOrNull { className ->
try {
classLoader.loadClass(className).takeIf { it.isEnum }?.also {
context.log.verbose("Successfully loaded enum class: $className")
}
} catch (e: Exception) {
null
}
}
if (enumC3559qCClass == null) {
context.log.error("Friend source enum class not found after trying: ${classNamesToTry.joinToString()}")
context.longToast("Failed to add friend: Required enum class not found")
return@runCatching
}
val enumConstants = enumC3559qCClass.enumConstants
?: enumC3559qCClass.getMethod("values").invoke(null) as? Array<*>
?: run {
context.log.error("Could not retrieve enum constants from ${enumC3559qCClass.name}")
context.longToast("Failed to add friend: Enum constants not accessible")
return@runCatching
}
context.log.verbose("Found ${enumConstants.size} enum constants")
val addedByUsername = enumConstants.firstOrNull { it.toString() == "ADDED_BY_USERNAME" }
?: enumConstants.firstOrNull { it.toString().contains("USERNAME", ignoreCase = true) }
?: enumConstants.firstOrNull()
?: run {
context.log.error("No enum constants available")
context.longToast("Failed to add friend: No valid enum constant found")
return@runCatching
}
context.log.verbose("Using enum constant: $addedByUsername")
val sQ7Default = loadStaticDefaultField(classLoader, "sQ7", "f288251e0")
val zQ7Default = loadStaticDefaultField(classLoader, "ZQ7", "f159958F0")
val iBgClass = try {
classLoader.loadClass("iBg")
} catch (e: Exception) {
context.log.error("Could not load iBg class: ${e.message}")
null
}
if (iBgClass == null) {
context.log.error("iBg class not found")
return@runCatching
}
val m51157aMethod = iBgClass.declaredMethods.firstOrNull { it.name == "m51157a" }
?: iBgClass.methods.firstOrNull { it.name == "m51157a" }
?: iBgClass.declaredMethods.firstOrNull { method ->
java.lang.reflect.Modifier.isStatic(method.modifiers) &&
method.parameterTypes.size == 14 &&
method.parameterTypes[0].isAssignableFrom(friendRelationshipChangerInstance.javaClass) &&
method.parameterTypes[1] == String::class.java &&
method.parameterTypes[2] == enumC3559qCClass
}
if (m51157aMethod == null) {
context.log.error("Could not find iBg.m51157a method")
context.log.error("Available static methods with 14 params:")
iBgClass.declaredMethods.filter {
java.lang.reflect.Modifier.isStatic(it.modifiers) && it.parameterTypes.size == 14
}.forEach { method ->
context.log.error(" ${method.name}: ${method.parameterTypes.joinToString { it.simpleName }}")
}
return@runCatching
}
m51157aMethod.isAccessible = true
try {
m51157aMethod.invoke(
null,
friendRelationshipChangerInstance,
userId,
addedByUsername,
sQ7Default,
zQ7Default,
null,
null,
null,
null,
null,
null,
null,
null,
4064
)?.also { result ->
result.javaClass.methods
.find { it.name == "subscribe" && it.parameterCount == 0 }
?.apply { isAccessible = true }
?.invoke(result)
}
} catch (e: Exception) {
context.log.error("Exception during method invocation: ${e.javaClass.name}: ${e.message}")
e.cause?.let { cause ->
context.log.error("Cause: ${cause.javaClass.name}: ${cause.message}")
cause.stackTrace?.take(5)?.forEach {
context.log.error(" at $it")
}
}
throw e
}
}.onFailure {
context.log.error("Failed to add friend $userId", it)
context.longToast("Failed to add friend: ${it.message}")
}
}
private fun loadSuggestedFriends(
coroutineScope: CoroutineScope,
onLoaded: (List<String>) -> Unit
) {
coroutineScope.launch(Dispatchers.IO) {
val blacklist = getUserIdBlacklist()
val suggestedFriends = context.database.getAllFriends()
.filter { it.userId !in blacklist && it.friendLinkType == FriendLinkType.SUGGESTED.value }
.sortedByDescending { it.addedTimestamp }
.mapNotNull { it.userId }
withContext(Dispatchers.Main) {
onLoaded(suggestedFriends)
}
}
}
override fun onActivityCreate() {
context.event.subscribe(ActivityResultEvent::class) { event ->
if (event.requestCode == pendingPickerAction?.first) {
val pendingAction = pendingPickerAction ?: return@subscribe
this.pendingPickerAction = null
event.canceled = true
pendingAction.second(event.intent.data!!)
}
}
}
private fun exportFriends(
userIds: List<String>
) {
pendingPickerAction = Random.nextInt(0, 65535) to { data ->
context.androidContext.contentResolver.openOutputStream(data).use { output ->
output?.bufferedWriter()?.use { writer ->
userIds.forEach {
writer.write(it)
writer.newLine()
}
}
context.longToast("Exported ${userIds.size} friends!")
}
}
context.mainActivity?.startActivityForResult(
Intent.createChooser(
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "my_friends.txt")
},
"Select a location to save the file"
),
pendingPickerAction!!.first
)
}
private val userIdToSnapchatter = mutableMapOf<String, Snapchatter>()
private val uuidRegex = Regex("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")
private fun getUserIdBlacklist() = arrayOf(
context.database.myUserId,
@@ -304,21 +83,131 @@ class ManageFriendList : AbstractAction() {
"84ee8839-3911-492d-8b94-72dd80f3713a",
)
private fun addFriend(userId: String) {
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance
?: run {
context.longToast("Failed to add friend: FriendRelationshipChanger instance not available")
return
}
runCatching {
val classLoader = context.androidContext.classLoader
val friendRelationshipChangerClass = friendRelationshipChangerInstance.javaClass
// Helper function to find static field by trying multiple field names
fun findStaticField(clazz: Class<*>, fieldNames: List<String>): Any? {
fieldNames.forEach { fieldName ->
runCatching {
clazz.getDeclaredField(fieldName).apply { isAccessible = true }.get(null)
}.getOrNull()?.let { return it }
}
// Fallback: find any static field of the same type
return clazz.declaredFields.firstOrNull { field ->
java.lang.reflect.Modifier.isStatic(field.modifiers) && field.type == clazz
}?.let { field ->
runCatching {
field.isAccessible = true
field.get(null)?.takeIf { it.javaClass == clazz }
}.getOrNull()
}
}
// Load F9l class
val f9lClass = classLoader.loadClass("F9l")
// Find the add friend method by matching signature
val method = (f9lClass.declaredMethods + f9lClass.methods).firstOrNull { method ->
if (!java.lang.reflect.Modifier.isStatic(method.modifiers) || method.parameterTypes.size != 14) return@firstOrNull false
val params = method.parameterTypes
params[0].isAssignableFrom(friendRelationshipChangerClass) &&
params[1] == String::class.java &&
params[2].isEnum
} ?: return@runCatching context.log.error("Could not find F9l.m8344a method")
// Extract classes from method signature
val enumClass = method.parameterTypes[2]
val tZ7Class = method.parameterTypes[3]
val g08Class = method.parameterTypes[4]
// Get enum constant for USERNAME
val enumConstants = enumClass.enumConstants ?: enumClass.getMethod("values").invoke(null) as? Array<*>
?: return@runCatching context.log.error("Could not get enum constants")
val addedByUsername = enumConstants.firstOrNull { it.toString().contains("USERNAME", ignoreCase = true) }
?: return@runCatching context.log.error("Could not find ADDED_BY_USERNAME enum")
// Get static field instances
val tZ7Default = findStaticField(tZ7Class, listOf("f301161j0", "f301158a", "f301165n0", "f301160c", "f301171t"))
?: return@runCatching context.log.error("Could not find tZ7 static field")
val g08Default = findStaticField(g08Class, listOf("f211750K0", "f211807x1", "f211805w1", "f211774f1", "f211775g1", "f211760U0", "f211783l1", "f211785m1", "f211787n1", "f211772d1"))
?: return@runCatching context.log.error("Could not find g08 static field")
// Invoke method with 14 parameters
method.isAccessible = true
val result = method.invoke(
null,
friendRelationshipChangerInstance,
userId,
addedByUsername,
tZ7Default,
g08Default,
null, null, null, null, null, // String params 6-10
null, // InteractionPlacementInfo
null, // String str7
null, // Integer num
4064 // int flags
)
// Subscribe to the Completable result
result?.javaClass?.methods?.firstOrNull {
it.name == "subscribe" && it.parameterCount == 0
}?.let { subscribeMethod ->
subscribeMethod.isAccessible = true
subscribeMethod.invoke(result)
}
}.onFailure {
context.log.error("Failed to add friend $userId", it)
context.longToast("Failed to add friend: ${it.message}")
}
}
override fun onActivityCreate() {
context.event.subscribe(ActivityResultEvent::class) { event ->
pendingPickerAction?.takeIf { it.first == event.requestCode }?.let {
pendingPickerAction = null
event.canceled = true
it.second(event.intent.data!!)
}
}
}
private fun exportFriends(userIds: List<String>) {
pendingPickerAction = Random.nextInt(0, 65535) to { data ->
context.androidContext.contentResolver.openOutputStream(data)?.bufferedWriter()?.use { writer ->
userIds.forEach { writer.write(it); writer.newLine() }
}
context.longToast("Exported ${userIds.size} friends!")
}
context.mainActivity?.startActivityForResult(
Intent.createChooser(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "my_friends.txt")
}, "Select a location to save the file"),
pendingPickerAction!!.first
)
}
private val userIdToSnapchatter = mutableMapOf<String, Snapchatter>()
@Composable
private fun ManagerDialog() {
val pendingFriendRequests = remember { mutableStateMapOf<String, Job>() }
var fetchedFriends by remember { mutableStateOf<List<String>?>(null) }
val coroutineScope = rememberCoroutineScope()
val openSuggestedOnLaunch = remember { consumeOpenSuggestedOnLaunch() }
val bitmojiCache = remember { me.eternal.purrfectsnap.core.util.EvictingMap<String, Bitmap>(50) }
val bitmojiCache = remember { EvictingMap<String, Bitmap>(50) }
val noBitmojiBitmap = remember { BitmapFactory.decodeResource(context.resources, android.R.drawable.ic_menu_report_image).asImageBitmap() }
LaunchedEffect(openSuggestedOnLaunch) {
if (openSuggestedOnLaunch) {
loadSuggestedFriends(coroutineScope) { fetchedFriends = it }
}
}
Box(
modifier = Modifier
.fillMaxWidth()
@@ -425,50 +314,38 @@ class ManageFriendList : AbstractAction() {
) {
pendingPickerAction = Random.nextInt(0, 65535) to { data ->
runCatching {
fetchedFriends = null
context.androidContext.contentResolver.openInputStream(data).use { input ->
fetchedFriends = input?.bufferedReader()?.readLines()?.filter {
it.matches(uuidRegex)
}?.map { it.trim() }?.toMutableList() ?: mutableListOf()
}
fetchedFriends = context.androidContext.contentResolver.openInputStream(data)?.bufferedReader()?.readLines()?.filter { it.matches(uuidRegex) }?.map { it.trim() }?.toMutableList() ?: mutableListOf()
}.onFailure {
context.log.error("Failed to import friends", it)
context.longToast("Failed to import friends: ${it.message}")
}
}
context.mainActivity?.startActivityForResult(
Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" },
"Select a file"
),
pendingPickerAction!!.first
)
context.mainActivity?.startActivityForResult(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" }, "Select a file"), pendingPickerAction!!.first)
}
}
PrimaryButton(
text = "Load Suggested Friends",
text = translation.get("load_suggested_friends"),
modifier = Modifier.fillMaxWidth()
) {
loadSuggestedFriends(coroutineScope) { fetchedFriends = it }
coroutineScope.launch(Dispatchers.IO) {
val blacklist = getUserIdBlacklist()
val suggestedFriends = context.database.getAllFriends().filter { it.userId !in blacklist && it.friendLinkType == FriendLinkType.SUGGESTED.value }.sortedByDescending { it.addedTimestamp }.mapNotNull { it.userId }
withContext(Dispatchers.Main) { fetchedFriends = suggestedFriends }
}
}
}
} else {
var searchQuery by remember { mutableStateOf("") }
val filteredFriends = remember(fetchedFriends, searchQuery) {
val friends = fetchedFriends ?: emptyList()
if (searchQuery.isBlank()) {
friends.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L }
} else {
friends.filter { userId ->
val friendInfo = context.database.getFriendInfo(userId)
friendInfo?.mutableUsername?.contains(searchQuery, ignoreCase = true) == true ||
friendInfo?.displayName?.contains(searchQuery, ignoreCase = true) == true ||
userId.contains(searchQuery, ignoreCase = true)
}.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L }
}
val sorted = { list: List<String> -> list.sortedByDescending { context.database.getFriendInfo(it)?.addedTimestamp ?: 0L } }
if (searchQuery.isBlank()) sorted(friends) else sorted(friends.filter { userId ->
val info = context.database.getFriendInfo(userId)
info?.mutableUsername?.contains(searchQuery, ignoreCase = true) == true || info?.displayName?.contains(searchQuery, ignoreCase = true) == true || userId.contains(searchQuery, ignoreCase = true)
})
}
Column(
modifier = Modifier
.fillMaxWidth()
@@ -511,7 +388,7 @@ class ManageFriendList : AbstractAction() {
}
Spacer(modifier = Modifier.size(46.dp))
}
BasicTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
@@ -572,64 +449,52 @@ class ManageFriendList : AbstractAction() {
}
}
items(filteredFriends) { userId ->
val friendInfo = remember(userId) { context.database.getFriendInfo(userId) }
val linkType = remember(friendInfo) {
friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
}
val isActuallyAdded = remember(friendInfo, linkType) {
friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING)
}
var friendInfo by remember(userId) { mutableStateOf(context.database.getFriendInfo(userId)) }
var friendLinkType by remember(userId) { mutableStateOf(friendInfo?.let { FriendLinkType.fromValue(it.friendLinkType) }) }
val isActuallyAdded = friendInfo?.let { info ->
friendLinkType != null && info.addedTimestamp > 0 &&
(friendLinkType == FriendLinkType.MUTUAL || friendLinkType == FriendLinkType.OUTGOING)
} ?: false
var friendSnapchatter by remember(userId) { mutableStateOf<Snapchatter?>(null) }
var friendLinkType by remember(userId) { mutableStateOf(linkType) }
var actuallyAdded by remember(userId) { mutableStateOf(isActuallyAdded) }
var bitmojiBitmap by remember(userId, friendInfo?.bitmojiAvatarId) {
mutableStateOf(friendInfo?.bitmojiAvatarId?.let { bitmojiCache[it] })
}
LaunchedEffect(userId) {
if (friendSnapchatter == null && !userIdToSnapchatter.containsKey(userId)) {
friendSnapchatter = userIdToSnapchatter[userId] ?: run {
withContext(Dispatchers.IO) {
context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull()?.let {
context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull()?.also {
userIdToSnapchatter[userId] = it
friendSnapchatter = it
}
}
} else {
friendSnapchatter = userIdToSnapchatter[userId]
}
while (true) {
delay(2000)
val newLinkType = friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
if (newLinkType != friendLinkType) {
friendLinkType = newLinkType
}
val newActuallyAdded = friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING)
if (newActuallyAdded != actuallyAdded) {
actuallyAdded = newActuallyAdded
}
}
}
LaunchedEffect(Unit) {
while (true) {
delay(2000)
context.database.getFriendInfo(userId)?.let {
friendInfo = it
FriendLinkType.fromValue(it.friendLinkType)?.let { newType ->
if (newType != friendLinkType) friendLinkType = newType
}
}
}
}
var bitmojiBitmap by remember(userId, friendInfo?.bitmojiAvatarId) {
mutableStateOf(friendInfo?.bitmojiAvatarId?.let { bitmojiCache[it] })
}
LaunchedEffect(userId, friendInfo?.bitmojiAvatarId, friendInfo?.bitmojiSelfieId) {
if (bitmojiBitmap != null || friendInfo?.bitmojiAvatarId == null || friendInfo?.bitmojiSelfieId == null) return@LaunchedEffect
withContext(Dispatchers.IO) {
val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(
friendInfo.bitmojiSelfieId,
friendInfo.bitmojiAvatarId,
BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D
) ?: return@withContext
runCatching {
RemoteMediaResolver.downloadMedia(bitmojiUrl) { inputStream, length ->
val avatarId = friendInfo.bitmojiAvatarId ?: return@downloadMedia
bitmojiCache[avatarId] = BitmapFactory.decodeStream(inputStream).also {
bitmojiBitmap = it
val info = friendInfo ?: return@LaunchedEffect
val avatarId = info.bitmojiAvatarId ?: return@LaunchedEffect
val selfieId = info.bitmojiSelfieId ?: return@LaunchedEffect
if (bitmojiBitmap != null) return@LaunchedEffect
BitmojiSelfie.getBitmojiSelfie(selfieId, avatarId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D)?.let { url ->
withContext(Dispatchers.IO) {
runCatching {
RemoteMediaResolver.downloadMedia(url) { inputStream, _ ->
bitmojiCache[avatarId] = BitmapFactory.decodeStream(inputStream).also { bitmojiBitmap = it }
}
}
}
@@ -652,7 +517,7 @@ class ManageFriendList : AbstractAction() {
contentDescription = null,
modifier = Modifier.size(35.dp)
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp)
@@ -680,39 +545,38 @@ class ManageFriendList : AbstractAction() {
color = if (type == FriendLinkType.MUTUAL) Color(0xFF8EF0F3) else Color(0xFFD9D3FF)
)
}
if (friendSnapchatter != null) {
val isPending = pendingFriendRequests.containsKey(userId) && pendingFriendRequests[userId]?.isActive != false
friendSnapchatter?.let {
val isFollowing = friendLinkType == FriendLinkType.FOLLOWING
val isPending = pendingFriendRequests[userId]?.isActive == true
val canAdd = !isActuallyAdded && !isPending && !isFollowing
PrimaryButton(
text = when {
isFollowing -> "Following"
actuallyAdded -> context.translation["common.added"]
isPending -> translation.get("adding") ?: "Adding..."
isActuallyAdded -> context.translation["common.added"]
isPending -> translation.get("adding")
else -> translation.get("add")
},
modifier = Modifier.widthIn(min = 110.dp),
enabled = !actuallyAdded && !isPending && !isFollowing
enabled = canAdd
) {
if (actuallyAdded || isPending || isFollowing) return@PrimaryButton
if (!canAdd) return@PrimaryButton
val prevLinkType = friendLinkType
addFriend(userId)
val job = coroutineScope.launch {
pendingFriendRequests[userId] = coroutineScope.launch(Dispatchers.IO) {
withTimeout(10000) {
while (friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }?.value == prevLinkType?.value) {
while (true) {
context.database.getFriendInfo(userId)?.let { updated ->
FriendLinkType.fromValue(updated.friendLinkType)?.takeIf { it != prevLinkType }?.let {
friendInfo = updated
friendLinkType = it
return@withTimeout
}
}
delay(500)
}
}
}.apply {
invokeOnCompletion {
pendingFriendRequests.remove(userId)
friendLinkType = friendInfo?.friendLinkType?.let { FriendLinkType.fromValue(it) }
actuallyAdded = isActuallyAdded || (friendInfo != null && linkType != null && friendInfo.addedTimestamp > 0 &&
(linkType == FriendLinkType.MUTUAL || linkType == FriendLinkType.OUTGOING))
}
}
pendingFriendRequests[userId] = job
}.apply { invokeOnCompletion { pendingFriendRequests.remove(userId) } }
}
if (isPending) {
CircularProgressIndicator(
@@ -805,4 +669,4 @@ class ManageFriendList : AbstractAction() {
}
}
}
}
}

View File

@@ -2,7 +2,9 @@ package me.eternal.purrfectsnap.core.features.impl.messaging
import me.eternal.purrfectsnap.common.data.NotificationType
import me.eternal.purrfectsnap.common.util.protobuf.ProtoEditor
import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader
import me.eternal.purrfectsnap.core.event.events.impl.NativeUnaryCallEvent
import me.eternal.purrfectsnap.core.event.events.impl.UnaryCallEvent
import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.util.hook.HookStage
@@ -12,9 +14,9 @@ class PreventMessageSending : Feature("Prevent message sending") {
override fun init() {
val preventMessageSending by context.config.messaging.preventMessageSending
context.event.subscribe(NativeUnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event ->
if (event.uri != "/messagingcoreservice.MessagingCoreService/UpdateContentMessage") return@subscribe
event.buffer = ProtoEditor(event.buffer).apply {
fun handleUpdateContentMessage(uri: String, buffer: ByteArray): ByteArray? {
if (uri != "/messagingcoreservice.MessagingCoreService/UpdateContentMessage") return null
return ProtoEditor(buffer).apply {
edit(3) {
// replace replayed to read receipt
if (firstOrNull(13) != null) {
@@ -25,6 +27,36 @@ class PreventMessageSending : Feature("Prevent message sending") {
}.toByteArray()
}
fun handleCreateContentMessage(uri: String, buffer: ByteArray): Boolean {
if (uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return false
val reader = ProtoReader(buffer)
// check for missed audio/video call in MessageContent (field 4)
val contentReader = reader.followPath(4) ?: return false
// try both possible field IDs based on SnapEnums and ContentType.java
val contentType = contentReader.getVarInt(2)
val isMissedAudio = contentType == 13L || contentType == 18L
val isMissedVideo = contentType == 12L || contentType == 17L
if (isMissedAudio && preventMessageSending.contains("abandon_audio")) return true
if (isMissedVideo && preventMessageSending.contains("abandon_video")) return true
return false
}
arrayOf(NativeUnaryCallEvent::class, UnaryCallEvent::class).forEach { eventClass ->
context.event.subscribe(eventClass) { event ->
val uri = if (event is NativeUnaryCallEvent) event.uri else (event as UnaryCallEvent).uri
if (!uri.startsWith("/messagingcoreservice.MessagingCoreService/")) return@subscribe
if (handleCreateContentMessage(uri, event.buffer)) {
event.canceled = true
}
handleUpdateContentMessage(uri, event.buffer)?.let { event.buffer = it }
}
}
context.classCache.conversationManager.hook("updateMessage", HookStage.BEFORE) { param ->
val messageUpdate = param.arg<Any>(2).toString()
if (messageUpdate == "SCREENSHOT" && preventMessageSending.contains("chat_screenshot")) {
@@ -34,6 +66,10 @@ class PreventMessageSending : Feature("Prevent message sending") {
if (messageUpdate == "SCREEN_RECORD" && preventMessageSending.contains("chat_screen_record")) {
param.setResult(null)
}
if ((messageUpdate == "REPLAY" || messageUpdate == "replay") && preventMessageSending.contains("snap_replay")) {
param.setResult(null)
}
}
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
@@ -41,9 +77,8 @@ class PreventMessageSending : Feature("Prevent message sending") {
val associatedType = NotificationType.fromContentType(contentType ?: return@subscribe) ?: return@subscribe
if (preventMessageSending.contains(associatedType.key)) {
context.log.verbose("Preventing message sending for $associatedType")
event.canceled = true
}
}
}
}
}

View File

@@ -94,6 +94,7 @@ class MessageSender(
it.conversations = conversations.toCollection(ArrayList())
it.mPhoneNumbers = arrayListOf<Any>()
it.stories = arrayListOf<Any>()
it.massSnaps = arrayListOf<Any>()
}
sendMessageWithContentMethod.invoke(context.feature(Messaging::class).conversationManager?.instanceNonNull(), messageDestinations.instanceNonNull(), localMessageContent, callback)

View File

@@ -1,6 +1,6 @@
package me.eternal.purrfectsnap.core.wrapper.impl
import me.eternal.purrfectsnap.core.PurrfectSnap
import me.eternal.purrfectsnap.core.purrfectsnap
import me.eternal.purrfectsnap.core.util.ktx.getObjectField
import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper
import java.nio.ByteBuffer
@@ -29,12 +29,14 @@ class SnapUUID(
obj
}
obj is UUID -> obj.toBytes()
PurrfectSnap.classCache.snapUUID.isInstance(obj) -> {
purrfectsnap.classCache.snapUUID.isInstance(obj) -> {
obj?.getObjectField("mId") as ByteArray
}
PurrfectSnap.classCache.snapShimsUUID?.isInstance(obj) == true -> {
purrfectsnap.classCache.snapShimsUUID?.isInstance(obj) == true -> {
val any = obj as Any
any::class.java.methods.firstOrNull { it.name == "getId" }?.invoke(any) as? ByteArray ?: ByteArray(16)
runCatching { any.javaClass.getMethod("getId").invoke(any) as ByteArray }.getOrElse {
any.getObjectField("mId") as ByteArray
}
}
else -> ByteArray(16)
}
@@ -44,7 +46,7 @@ class SnapUUID(
override var instance: Any?
set(_) {}
get() = PurrfectSnap.classCache.snapUUID.getConstructor(ByteArray::class.java).newInstance(uuidBytes)
get() = purrfectsnap.classCache.snapUUID.getConstructor(ByteArray::class.java).newInstance(uuidBytes)
override fun toString(): String {
return uuidString
@@ -60,3 +62,4 @@ class SnapUUID(
return uuidBytes.contentHashCode()
}
}