feat(actions): manage friend list

This commit is contained in:
rhunk
2024-03-17 23:43:25 +01:00
parent 31c6bef10f
commit f53e2db68d
5 changed files with 286 additions and 17 deletions

View File

@ -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.ExportChatMessages
import me.rhunk.snapenhance.core.action.impl.ExportMemories
import me.rhunk.snapenhance.core.action.impl.ManageFriendList
class ActionManager(
private val modContext: ModContext,
@ -17,6 +18,7 @@ class ActionManager(
EnumAction.CLEAN_CACHE to CleanCache(),
EnumAction.EXPORT_CHAT_MESSAGES to ExportChatMessages(),
EnumAction.BULK_MESSAGING_ACTION to BulkMessagingAction(),
EnumAction.MANAGE_FRIEND_LIST to ManageFriendList(),
EnumAction.EXPORT_MEMORIES to ExportMemories(),
).map {
it.key to it.value.apply {

View File

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