commit f66859b1fd4cf59c471837fc25afd9c47f238f59
parent 368878abd7c218494a0c314836817657ddab24c8
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sat, 25 Nov 2023 22:15:35 +0100

feat(action): bulk remove friends

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 35++++++++++++++++++++++++-----------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt | 3++-
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/database/impl/FriendInfo.kt | 4++--
Acore/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkRemoveFriends.kt | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt | 19+++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt | 14++++++++++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/ActionManager.kt | 2++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt | 2+-
Mmapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt | 11++++++++++-
9 files changed, 184 insertions(+), 20 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -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,16 +836,28 @@ "snapchat_plus_state": { "subscribed": "Subscribed", "not_subscribed": "Not Subscribed" - }, - "friendship_link_type": { - "mutual": "Mutual", - "outgoing": "Outgoing", - "blocked": "Blocked", - "deleted": "Deleted", - "following": "Following", - "suggested": "Suggested", - "incoming": "Incoming", - "incoming_follower": "Incoming Follower" + } + }, + + "friendship_link_type": { + "mutual": "Mutual", + "outgoing": "Outgoing", + "blocked": "Blocked", + "deleted": "Deleted", + "following": "Following", + "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." } }, diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/action/EnumAction.kt @@ -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" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/database/impl/FriendInfo.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/database/impl/FriendInfo.kt @@ -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, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkRemoveFriends.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkRemoveFriends.kt @@ -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) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt @@ -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( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AddFriendSourceSpoof.kt @@ -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 { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/ActionManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/ActionManager.kt @@ -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 diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt @@ -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 { diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/FriendRelationshipChangerMapper.kt @@ -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 }