multiple fixes
This commit is contained in:
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user