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:
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
}