mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
feat(actions): manage friend list
This commit is contained in:
@ -134,6 +134,10 @@
|
|||||||
"name": "Clean Snapchat Cache",
|
"name": "Clean Snapchat Cache",
|
||||||
"description": "Cleans the Snapchat Cache"
|
"description": "Cleans the Snapchat Cache"
|
||||||
},
|
},
|
||||||
|
"manage_friend_list": {
|
||||||
|
"name": "Manage Friend List",
|
||||||
|
"description": "Import/export your friends list when backing up"
|
||||||
|
},
|
||||||
"export_chat_messages": {
|
"export_chat_messages": {
|
||||||
"name": "Export Chat Messages",
|
"name": "Export Chat Messages",
|
||||||
"description": "Exports conversation messages into a JSON/HTML/TXT file"
|
"description": "Exports conversation messages into a JSON/HTML/TXT file"
|
||||||
|
@ -9,6 +9,7 @@ enum class EnumAction(
|
|||||||
EXPORT_CHAT_MESSAGES("export_chat_messages"),
|
EXPORT_CHAT_MESSAGES("export_chat_messages"),
|
||||||
EXPORT_MEMORIES("export_memories"),
|
EXPORT_MEMORIES("export_memories"),
|
||||||
BULK_MESSAGING_ACTION("bulk_messaging_action"),
|
BULK_MESSAGING_ACTION("bulk_messaging_action"),
|
||||||
|
MANAGE_FRIEND_LIST("manage_friend_list"),
|
||||||
CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true);
|
CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -7,6 +7,7 @@ import me.rhunk.snapenhance.core.action.impl.BulkMessagingAction
|
|||||||
import me.rhunk.snapenhance.core.action.impl.CleanCache
|
import me.rhunk.snapenhance.core.action.impl.CleanCache
|
||||||
import me.rhunk.snapenhance.core.action.impl.ExportChatMessages
|
import me.rhunk.snapenhance.core.action.impl.ExportChatMessages
|
||||||
import me.rhunk.snapenhance.core.action.impl.ExportMemories
|
import me.rhunk.snapenhance.core.action.impl.ExportMemories
|
||||||
|
import me.rhunk.snapenhance.core.action.impl.ManageFriendList
|
||||||
|
|
||||||
class ActionManager(
|
class ActionManager(
|
||||||
private val modContext: ModContext,
|
private val modContext: ModContext,
|
||||||
@ -17,6 +18,7 @@ class ActionManager(
|
|||||||
EnumAction.CLEAN_CACHE to CleanCache(),
|
EnumAction.CLEAN_CACHE to CleanCache(),
|
||||||
EnumAction.EXPORT_CHAT_MESSAGES to ExportChatMessages(),
|
EnumAction.EXPORT_CHAT_MESSAGES to ExportChatMessages(),
|
||||||
EnumAction.BULK_MESSAGING_ACTION to BulkMessagingAction(),
|
EnumAction.BULK_MESSAGING_ACTION to BulkMessagingAction(),
|
||||||
|
EnumAction.MANAGE_FRIEND_LIST to ManageFriendList(),
|
||||||
EnumAction.EXPORT_MEMORIES to ExportMemories(),
|
EnumAction.EXPORT_MEMORIES to ExportMemories(),
|
||||||
).map {
|
).map {
|
||||||
it.key to it.value.apply {
|
it.key to it.value.apply {
|
||||||
|
@ -0,0 +1,273 @@
|
|||||||
|
package me.rhunk.snapenhance.core.action.impl
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
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.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import me.rhunk.snapenhance.common.action.EnumAction
|
||||||
|
import me.rhunk.snapenhance.common.data.FriendLinkType
|
||||||
|
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
|
||||||
|
import me.rhunk.snapenhance.core.action.AbstractAction
|
||||||
|
import me.rhunk.snapenhance.core.event.events.impl.ActivityResultEvent
|
||||||
|
import me.rhunk.snapenhance.core.features.impl.experiments.AddFriendSourceSpoof
|
||||||
|
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||||
|
import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter
|
||||||
|
import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
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 addFriend(userId: String) {
|
||||||
|
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance
|
||||||
|
context.mappings.useMapper(FriendRelationshipChangerMapper::class) {
|
||||||
|
val addFriend = friendshipRelationshipChangerKtx.get()?.methods?.firstOrNull { it.name == addFriendMethod.get() }
|
||||||
|
?: return@useMapper
|
||||||
|
|
||||||
|
addFriend.invoke(
|
||||||
|
null,
|
||||||
|
friendRelationshipChangerInstance,
|
||||||
|
userId,
|
||||||
|
addFriend.parameterTypes[2].enumConstants.first { it.toString() == "ADDED_BY_USERNAME" },
|
||||||
|
addFriend.parameterTypes[3].enumConstants.first { it.toString() == "SEARCH" },
|
||||||
|
addFriend.parameterTypes[4].enumConstants.first { it.toString() == "SEARCH" },
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreate() {
|
||||||
|
context.runOnUiThread {
|
||||||
|
context.actionManager.execute(EnumAction.MANAGE_FRIEND_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ManagerDialog() {
|
||||||
|
val pendingFriendRequests = remember { mutableStateMapOf<String, Job>() }
|
||||||
|
var fetchedFriends by remember { mutableStateOf<List<String>?>(null) } // list of uuids
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
if (fetchedFriends == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 200.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text("Manage Friend List", fontSize = 20.sp)
|
||||||
|
Spacer(modifier = Modifier.height(5.dp))
|
||||||
|
Text(
|
||||||
|
text = "Export friends allows you to save a list of your friends' IDs in a text file. Importing from a file will display the friends in a list where you can add them.",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
exportFriends(context.database.getAllFriends().filter { it.friendLinkType == FriendLinkType.MUTUAL.value && it.addedTimestamp > 0L }.mapNotNull { it.userId })
|
||||||
|
}) {
|
||||||
|
Text("Export friends")
|
||||||
|
}
|
||||||
|
Button(onClick = {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
context.log.error("Failed to import friends", it)
|
||||||
|
context.longToast("Failed to import friends: ${it.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// launch file picker
|
||||||
|
context.mainActivity?.startActivityForResult(
|
||||||
|
Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" },
|
||||||
|
"Select a file"
|
||||||
|
),
|
||||||
|
pendingPickerAction!!.first
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Text("Import from file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
onClick = {
|
||||||
|
fetchedFriends = null
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f).padding(8.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
if (fetchedFriends?.isEmpty() == true) {
|
||||||
|
Text("No friends found", modifier = Modifier.padding(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(fetchedFriends ?: emptyList()) { userId ->
|
||||||
|
fun fetchLocalLinkType(): FriendLinkType? {
|
||||||
|
return context.database.getFriendInfo(userId)?.friendLinkType?.let { FriendLinkType.fromValue(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var friendSnapchatter by remember(userId) { mutableStateOf<Snapchatter?>(null) }
|
||||||
|
var failedToFetch by remember(userId) { mutableStateOf(false) }
|
||||||
|
var friendLinkType by remember(userId) { mutableStateOf(fetchLocalLinkType()) }
|
||||||
|
|
||||||
|
LaunchedEffect(userId) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
friendSnapchatter = userIdToSnapchatter.getOrPut(userId) {
|
||||||
|
context.feature(Messaging::class).fetchSnapchatterInfos(listOf(userId)).firstOrNull() ?: run {
|
||||||
|
failedToFetch = true
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
){
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
friendSnapchatter?.let { snapchatter ->
|
||||||
|
Text(snapchatter.displayName?.let { "$it (${snapchatter.username}) " } ?: snapchatter.username ?: "Unknown")
|
||||||
|
}
|
||||||
|
Text(userId, fontSize = 12.sp, fontWeight = FontWeight.Light)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (friendSnapchatter != null && friendLinkType != FriendLinkType.FOLLOWING) {
|
||||||
|
Button(
|
||||||
|
enabled = friendLinkType != FriendLinkType.MUTUAL,
|
||||||
|
onClick = {
|
||||||
|
val prevLinkType = fetchLocalLinkType()
|
||||||
|
if (prevLinkType == FriendLinkType.MUTUAL || pendingFriendRequests[userId]?.isActive == true) return@Button
|
||||||
|
addFriend(userId)
|
||||||
|
pendingFriendRequests[userId] = coroutineScope.launch {
|
||||||
|
withTimeout(10000) {
|
||||||
|
while (fetchLocalLinkType()?.value == prevLinkType?.value) {
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
invokeOnCompletion {
|
||||||
|
pendingFriendRequests.remove(userId)
|
||||||
|
friendLinkType = fetchLocalLinkType()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (friendLinkType == FriendLinkType.MUTUAL) {
|
||||||
|
Text("Added")
|
||||||
|
} else if (pendingFriendRequests[userId]?.isActive == true) {
|
||||||
|
CircularProgressIndicator(color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.size(20.dp), strokeWidth = 1.dp)
|
||||||
|
} else {
|
||||||
|
Text("Add")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
context.coroutineScope.launch(Dispatchers.Main) {
|
||||||
|
createComposeAlertDialog(context.mainActivity!!) {
|
||||||
|
ManagerDialog()
|
||||||
|
}.apply {
|
||||||
|
setCanceledOnTouchOutside(false)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,18 +18,7 @@ class FriendRelationshipChangerMapper : AbstractClassMapper("FriendRelationshipC
|
|||||||
mapper {
|
mapper {
|
||||||
for (classDef in classes) {
|
for (classDef in classes) {
|
||||||
classDef.methods.firstOrNull { it.name == "<init>" }?.implementation?.findConstString("FriendRelationshipChangerImpl")?.takeIf { it } ?: continue
|
classDef.methods.firstOrNull { it.name == "<init>" }?.implementation?.findConstString("FriendRelationshipChangerImpl")?.takeIf { it } ?: continue
|
||||||
val addFriendDexMethod = classDef.methods.first {
|
classReference.set(classDef.getClassName())
|
||||||
it.parameterTypes.size > 4 &&
|
|
||||||
getClass(it.parameterTypes[1])?.isEnum() == true &&
|
|
||||||
getClass(it.parameterTypes[2])?.isEnum() == true &&
|
|
||||||
getClass(it.parameterTypes[3])?.isEnum() == true &&
|
|
||||||
it.parameters[4].type == "Ljava/lang/String;"
|
|
||||||
}
|
|
||||||
|
|
||||||
this@FriendRelationshipChangerMapper.apply {
|
|
||||||
classReference.set(classDef.getClassName())
|
|
||||||
}
|
|
||||||
|
|
||||||
return@mapper
|
return@mapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,11 +38,11 @@ class FriendRelationshipChangerMapper : AbstractClassMapper("FriendRelationshipC
|
|||||||
|
|
||||||
val addFriendDexMethod = classDef.methods.firstOrNull {
|
val addFriendDexMethod = classDef.methods.firstOrNull {
|
||||||
Modifier.isStatic(it.accessFlags) &&
|
Modifier.isStatic(it.accessFlags) &&
|
||||||
it.parameterTypes.size == 5 &&
|
it.parameterTypes.size == 6 &&
|
||||||
it.parameterTypes[1] == "Ljava/lang/String;" &&
|
it.parameterTypes[1] == "Ljava/lang/String;" &&
|
||||||
getClass(it.parameterTypes[2])?.isEnum() == true &&
|
getClass(it.parameterTypes[2])?.isEnum() == true &&
|
||||||
getClass(it.parameterTypes[4])?.isEnum() == true &&
|
getClass(it.parameterTypes[4])?.isEnum() == true &&
|
||||||
it.parameterTypes[5] == "I"
|
it.parameterTypes[5] == "I"
|
||||||
} ?: return@mapper
|
} ?: return@mapper
|
||||||
|
|
||||||
addFriendMethod.set(addFriendDexMethod.name)
|
addFriendMethod.set(addFriendDexMethod.name)
|
||||||
|
Reference in New Issue
Block a user