feat(action): bulk remove friends

This commit is contained in:
rhunk 2023-11-25 22:15:35 +01:00
parent 368878abd7
commit f66859b1fd
9 changed files with 183 additions and 20 deletions

View File

@ -115,7 +115,8 @@
"refresh_mappings": "Refresh Mappings",
"open_map": "Choose location on map",
"check_for_updates": "Check for updates",
"export_chat_messages": "Export Chat Messages"
"export_chat_messages": "Export Chat Messages",
"bulk_remove_friends": "Bulk Remove Friends"
},
"features": {
@ -835,7 +836,9 @@
"snapchat_plus_state": {
"subscribed": "Subscribed",
"not_subscribed": "Not Subscribed"
}
},
"friendship_link_type": {
"mutual": "Mutual",
"outgoing": "Outgoing",
@ -845,6 +848,16 @@
"suggested": "Suggested",
"incoming": "Incoming",
"incoming_follower": "Incoming Follower"
},
"bulk_remove_friends": {
"title": "Bulk Remove Friend",
"progress_status": "Removing friends {index} of {total}",
"selection_dialog_title": "Select friends to remove",
"selection_dialog_remove_button": "Remove Selection",
"confirmation_dialog": {
"title": "Are you sure?",
"message": "This will remove all selected friends. This action cannot be undone."
}
},

View File

@ -8,7 +8,8 @@ enum class EnumAction(
val isCritical: Boolean = false,
) {
CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true),
EXPORT_CHAT_MESSAGES("export_chat_messages");
EXPORT_CHAT_MESSAGES("export_chat_messages"),
BULK_REMOVE_FRIENDS("bulk_remove_friends");
companion object {
const val ACTION_PARAMETER = "se_action"

View File

@ -22,8 +22,8 @@ data class FriendInfo(
var friendmojiCategories: String? = null,
var snapScore: Int = 0,
var birthday: Long = 0,
var addedTimestamp: Long = 0,
var reverseAddedTimestamp: Long = 0,
var addedTimestamp: Long = -1,
var reverseAddedTimestamp: Long = -1,
var serverDisplayName: String? = null,
var streakLength: Int = 0,
var streakExpirationTimestamp: Long = 0,

View File

@ -0,0 +1,113 @@
package me.rhunk.snapenhance.core.action.impl
import android.widget.ProgressBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.common.data.FriendLinkType
import me.rhunk.snapenhance.core.action.AbstractAction
import me.rhunk.snapenhance.core.features.impl.experiments.AddFriendSourceSpoof
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
class BulkRemoveFriends : AbstractAction() {
private val translation by lazy { context.translation.getCategory("bulk_remove_friends") }
private fun removeFriends(friendIds: List<String>) {
var index = 0
val dialog = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle("...")
.setView(ProgressBar(context.mainActivity))
.setCancelable(false)
.show()
context.coroutineScope.launch {
friendIds.forEach {
removeFriend(it)
index++
withContext(Dispatchers.Main) {
dialog.setTitle(
translation.format("progress_status", "index" to index.toString(), "total" to friendIds.size.toString())
)
}
delay(500)
}
withContext(Dispatchers.Main) {
dialog.dismiss()
}
}
}
private fun confirmationDialog(onConfirm: () -> Unit) {
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle(translation["confirmation_dialog.title"])
.setMessage(translation["confirmation_dialog.message"])
.setPositiveButton(context.translation["button.positive"]) { _, _ ->
onConfirm()
}
.setNegativeButton(context.translation["button.negative"]) { _, _ -> }
.show()
}
override fun run() {
val userIdBlacklist = arrayOf(
context.database.myUserId,
"b42f1f70-5a8b-4c53-8c25-34e7ec9e6781", // myai
"84ee8839-3911-492d-8b94-72dd80f3713a", // teamsnapchat
)
context.coroutineScope.launch(Dispatchers.Main) {
val friends = context.database.getAllFriends().filter {
it.userId !in userIdBlacklist &&
it.addedTimestamp != -1L &&
it.friendLinkType == FriendLinkType.MUTUAL.value ||
it.friendLinkType == FriendLinkType.OUTGOING.value
}.sortedByDescending {
it.friendLinkType == FriendLinkType.OUTGOING.value
}
val selectedFriends = mutableListOf<String>()
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle(translation["selection_dialog_title"])
.setMultiChoiceItems(friends.map { friend ->
(friend.displayName?.let {
"$it (${friend.mutableUsername})"
} ?: friend.mutableUsername) +
": ${context.translation["friendship_link_type.${FriendLinkType.fromValue(friend.friendLinkType).shortName}"]}"
}.toTypedArray(), null) { _, which, isChecked ->
if (isChecked) {
selectedFriends.add(friends[which].userId!!)
} else {
selectedFriends.remove(friends[which].userId)
}
}
.setPositiveButton(translation["selection_dialog_remove_button"]) { _, _ ->
confirmationDialog {
removeFriends(selectedFriends)
}
}
.setNegativeButton(context.translation["button.cancel"]) { _, _ -> }
.show()
}
}
private fun removeFriend(userId: String) {
val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger")
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance!!
val removeFriendMethod = friendRelationshipChangerInstance::class.java.methods.first {
it.name == friendRelationshipChangerMapping["removeFriendMethod"].toString()
}
val completable = removeFriendMethod.invoke(friendRelationshipChangerInstance,
userId, // userId
removeFriendMethod.parameterTypes[1].enumConstants.first { it.toString() == "DELETED_BY_MY_FRIENDS" }, // source
null, // unknown
null, // unknown
null // InteractionPlacementInfo
)!!
completable::class.java.methods.first {
it.name == "subscribe" && it.parameterTypes.isEmpty()
}.invoke(completable)
}
}

View File

@ -187,6 +187,25 @@ class DatabaseAccess(
}
}
fun getAllFriends(): List<FriendInfo> {
return mainDb?.performOperation {
safeRawQuery(
"SELECT * FROM FriendWithUsername",
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 getFeedEntries(limit: Int): List<FriendFeedEntry> {
return mainDb?.performOperation {
safeRawQuery(

View File

@ -4,17 +4,23 @@ import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.hook.hookConstructor
class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() {
class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
var friendRelationshipChangerInstance: Any? = null
private set
override fun onActivityCreate() {
val friendRelationshipChangerMapping = context.mappings.getMappedMap("FriendRelationshipChanger")
findClass(friendRelationshipChangerMapping["class"].toString()).hookConstructor(HookStage.AFTER) { param ->
friendRelationshipChangerInstance = param.thisObject()
}
findClass(friendRelationshipChangerMapping["class"].toString())
.hook(friendRelationshipChangerMapping["addFriendMethod"].toString(), HookStage.BEFORE) { param ->
val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@hook
context.log.verbose("addFriendMethod: ${param.args().toList()}", featureKey)
fun setEnum(index: Int, value: String) {
val enumData = param.arg<Any>(index)
enumData::class.java.enumConstants.first { it.toString() == value }.let {

View File

@ -3,6 +3,7 @@ package me.rhunk.snapenhance.core.manager.impl
import android.content.Intent
import me.rhunk.snapenhance.common.action.EnumAction
import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.action.impl.BulkRemoveFriends
import me.rhunk.snapenhance.core.action.impl.CleanCache
import me.rhunk.snapenhance.core.action.impl.ExportChatMessages
import me.rhunk.snapenhance.core.manager.Manager
@ -15,6 +16,7 @@ class ActionManager(
mapOf(
EnumAction.CLEAN_CACHE to CleanCache::class,
EnumAction.EXPORT_CHAT_MESSAGES to ExportChatMessages::class,
EnumAction.BULK_REMOVE_FRIENDS to BulkRemoveFriends::class,
).map {
it.key to it.value.java.getConstructor().newInstance().apply {
this.context = modContext

View File

@ -94,7 +94,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
"day" to profile.birthday.toInt().toString())
},
translation["friendship"] to run {
translation.getCategory("friendship_link_type")[FriendLinkType.fromValue(profile.friendLinkType).shortName]
context.translation["friendship_link_type.${FriendLinkType.fromValue(profile.friendLinkType).shortName}"]
},
translation["add_source"] to context.database.getAddSource(profile.userId!!)?.takeIf { it.isNotEmpty() },
translation["snapchat_plus"] to run {

View File

@ -18,9 +18,18 @@ class FriendRelationshipChangerMapper : AbstractClassMapper() {
it.parameters[4].type == "Ljava/lang/String;"
}
val removeFriendMethod = classDef.methods.first {
it.parameterTypes.size == 5 &&
it.parameterTypes[0] == "Ljava/lang/String;" &&
getClass(it.parameterTypes[1])?.isEnum() == true &&
it.parameterTypes[2] == "Ljava/lang/String;" &&
it.parameterTypes[3] == "Ljava/lang/String;"
}
addMapping("FriendRelationshipChanger",
"class" to classDef.getClassName(),
"addFriendMethod" to addFriendMethod.name
"addFriendMethod" to addFriendMethod.name,
"removeFriendMethod" to removeFriendMethod.name
)
return@mapper
}