Friend request

This commit is contained in:
RSR/
2026-02-04 11:52:32 +04:00
parent 3ce4617deb
commit ff398ccc9a
4 changed files with 150 additions and 7 deletions

View File

@@ -2832,8 +2832,13 @@
"clear_conversations": "مسح المحادثات", "clear_conversations": "مسح المحادثات",
"clear_friend_feed": "مسح موجز الأصدقاء ({count})", "clear_friend_feed": "مسح موجز الأصدقاء ({count})",
"unfollow": "إلغاء المتابعة", "unfollow": "إلغاء المتابعة",
"remove": "إزالة" "remove": "إزالة",
"accept": "تم القبول",
"ignore": "تم التجاهل"
}, },
"accept_requests": "قبول الطلبات",
"ignore_requests": "تجاهل الطلبات",
"cleared_from_feed": "تم المسح من الخلاصة",
"leave_groups": "مغادرة {count} مجموعات", "leave_groups": "مغادرة {count} مجموعات",
"left_group_success": "تمت مغادرة المجموعة بنجاح", "left_group_success": "تمت مغادرة المجموعة بنجاح",
"failed_to_leave_group": "فشل مغادرة المجموعة: {error}", "failed_to_leave_group": "فشل مغادرة المجموعة: {error}",
@@ -2872,6 +2877,8 @@
"non_streaks": "بدون سلاسل", "non_streaks": "بدون سلاسل",
"followed": "المتابَعين", "followed": "المتابَعين",
"following": "تتابعهم", "following": "تتابعهم",
"incoming": "طلبات صداقة",
"incoming_follower": "طلبات متابعة",
"location_on_map": "الموقع على الخريطة" "location_on_map": "الموقع على الخريطة"
}, },
"sort_options": { "sort_options": {

View File

@@ -2831,8 +2831,13 @@
"clear_conversations": "Clear Conversations", "clear_conversations": "Clear Conversations",
"clear_friend_feed": "Clear Friend Feed ({count})", "clear_friend_feed": "Clear Friend Feed ({count})",
"unfollow": "Unfollow", "unfollow": "Unfollow",
"remove": "Remove" "remove": "Remove",
"accept": "Accepted",
"ignore": "Ignored"
}, },
"accept_requests": "Accept requests",
"ignore_requests": "Ignore requests",
"cleared_from_feed": "Cleared from feed",
"leave_groups": "Leave {count} groups", "leave_groups": "Leave {count} groups",
"left_group_success": "Left group successfully", "left_group_success": "Left group successfully",
"failed_to_leave_group": "Failed to leave group: {error}", "failed_to_leave_group": "Failed to leave group: {error}",
@@ -2871,6 +2876,8 @@
"non_streaks": "Non Streaks", "non_streaks": "Non Streaks",
"followed": "Followed", "followed": "Followed",
"following": "Following", "following": "Following",
"incoming": "Friend requests",
"incoming_follower": "Follower requests",
"location_on_map": "Location on Map" "location_on_map": "Location on Map"
}, },
"sort_options": { "sort_options": {

View File

@@ -95,6 +95,8 @@ class BulkMessagingAction : AbstractAction() {
STREAKS, STREAKS,
NON_STREAKS, NON_STREAKS,
FOLLOWING, FOLLOWING,
INCOMING,
INCOMING_FOLLOWER,
LOCATION_ON_MAP LOCATION_ON_MAP
} }
@@ -300,6 +302,8 @@ class BulkMessagingAction : AbstractAction() {
} }
isFollowing isFollowing
} }
Filter.INCOMING -> friend.friendLinkType == FriendLinkType.INCOMING.value
Filter.INCOMING_FOLLOWER -> friend.friendLinkType == FriendLinkType.INCOMING_FOLLOWER.value
Filter.LOCATION_ON_MAP -> betterLocation.locationHistory.contains(friend.userId) Filter.LOCATION_ON_MAP -> betterLocation.locationHistory.contains(friend.userId)
} && nameFilter.takeIf { it.isNotBlank() }?.let { name -> } && nameFilter.takeIf { it.isNotBlank() }?.let { name ->
friend.mutableUsername?.contains( friend.mutableUsername?.contains(
@@ -346,11 +350,31 @@ class BulkMessagingAction : AbstractAction() {
val myLocation = betterLocation.locationHistory[context.database.myUserId] val myLocation = betterLocation.locationHistory[context.database.myUserId]
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// Load friends if needed val friendIdsStillInFeed = runCatching {
context.database.getFeedEntries(Int.MAX_VALUE)
.filter { it.conversationType == 0 && it.participantsSize == 2 }
.mapNotNull { it.participants?.firstOrNull { id -> id != context.database.myUserId } }
.toSet()
}.getOrElse { emptySet() }
val incomingRequestUserIds = if (filter == Filter.INCOMING || filter == Filter.INCOMING_FOLLOWER) {
runCatching { context.database.getIncomingRequestUserIds() }.getOrElse { emptySet() }
} else emptySet()
val newFriends = if (conversationType == ConversationType.FRIENDS_ONLY || conversationType == ConversationType.BOTH) { val newFriends = if (conversationType == ConversationType.FRIENDS_ONLY || conversationType == ConversationType.BOTH) {
context.database.getAllFriends().let { friends -> context.database.getAllFriends().let { friends ->
filterFriends(friends, filter, nameFilter) filterFriends(friends, filter, nameFilter)
}.filter { it.userId?.let { id -> !hiddenFriendIds.contains(id) } == true }.toMutableList() }
.filter { it.userId?.let { id -> !hiddenFriendIds.contains(id) } == true }
.filter { friend ->
when {
filter == Filter.INCOMING || filter == Filter.INCOMING_FOLLOWER ->
friend.userId != null && friend.userId in incomingRequestUserIds
else ->
friendIdsStillInFeed.isEmpty() || friend.userId in friendIdsStillInFeed
}
}
.toMutableList()
} else mutableListOf() } else mutableListOf()
val newGroups = if (conversationType == ConversationType.GROUPS_ONLY || conversationType == ConversationType.BOTH) { val newGroups = if (conversationType == ConversationType.GROUPS_ONLY || conversationType == ConversationType.BOTH) {
@@ -1053,10 +1077,31 @@ class BulkMessagingAction : AbstractAction() {
} }
} }
} }
val incomingRequests = filter == Filter.INCOMING || filter == Filter.INCOMING_FOLLOWER
when (conversationType) { when (conversationType) {
ConversationType.FRIENDS_ONLY -> if (following) { ConversationType.FRIENDS_ONLY -> when {
addUnfollowActions("failed_to_fetch_conversations") following -> addUnfollowActions("failed_to_fetch_conversations")
} else { incomingRequests -> {
put({ "${translation["accept_requests"]} (${selectedFriends.size})" }) {
val ids = selectedFriends.toList()
selectedFriends.clear()
removeAction(ctx, ids, 500L to 1200L) { userId, setDialogMessage ->
acceptFriendRequest(userId)
setDialogMessage(translation["actions.accept"])
markRemoval(userId, null)
}.invokeOnCompletion { coroutineScope.launch { refreshList() } }
}
put({ "${translation["ignore_requests"]} (${selectedFriends.size})" }) {
val ids = selectedFriends.toList()
selectedFriends.clear()
removeAction(ctx, ids, 500L to 1200L) { userId, setDialogMessage ->
ignoreFriendRequest(userId)
setDialogMessage(translation["actions.ignore"])
markRemoval(userId, null)
}.invokeOnCompletion { coroutineScope.launch { refreshList() } }
}
}
else -> {
put({ translation.format("clean_conversations", "count" to selectedFriends.size.toString()) }) { put({ translation.format("clean_conversations", "count" to selectedFriends.size.toString()) }) {
context.feature(Messaging::class).conversationManager?.getOneOnOneConversationIds(selectedFriends.toList().also { selectedFriends.clear() }, onError = { error -> context.feature(Messaging::class).conversationManager?.getOneOnOneConversationIds(selectedFriends.toList().also { selectedFriends.clear() }, onError = { error ->
context.shortToast(translation.format("failed_to_fetch_conversations", "error" to error)) context.shortToast(translation.format("failed_to_fetch_conversations", "error" to error))
@@ -1117,6 +1162,7 @@ class BulkMessagingAction : AbstractAction() {
}.invokeOnCompletion { coroutineScope.launch { refreshList() } } }.invokeOnCompletion { coroutineScope.launch { refreshList() } }
}) })
} }
}
} }
ConversationType.GROUPS_ONLY -> if (!following) { ConversationType.GROUPS_ONLY -> if (!following) {
put({ translation.format("clean_group_conversations", "count" to selectedGroups.size.toString()) }) { put({ translation.format("clean_group_conversations", "count" to selectedGroups.size.toString()) }) {
@@ -1409,6 +1455,56 @@ class BulkMessagingAction : AbstractAction() {
} }
} }
private fun acceptFriendRequest(userId: String) {
val friendRelationshipChangerInstance = context.feature(AddFriendSourceSpoof::class).friendRelationshipChangerInstance
?: run {
context.log.error("Accept friend: FriendRelationshipChanger instance not available")
return
}
context.mappings.useMapper(FriendRelationshipChangerMapper::class) {
runCatching {
val f9lClass = helperClass.getAsClass() ?: return@runCatching context.log.error("Could not find FriendRelationshipChanger helper class")
val addFriendMethodName = addFriend14Method.get() ?: return@runCatching context.log.error("Could not find add friend method name")
val sourceTypeClass = sourceType.getAsClass() ?: return@runCatching context.log.error("Could not find source type class")
val pageTypeClass = pageType.getAsClass() ?: return@runCatching context.log.error("Could not find page type class")
val method = f9lClass.declaredMethods.firstOrNull { it.name == addFriendMethodName }
?: return@runCatching context.log.error("Could not find $addFriendMethodName method")
fun findStaticField(clazz: Class<*>): Any? = clazz.declaredFields.firstOrNull { field ->
java.lang.reflect.Modifier.isStatic(field.modifiers) && field.type == clazz
}?.let { field ->
runCatching { field.isAccessible = true; field.get(null)?.takeIf { it.javaClass == clazz } }.getOrNull()
}
val enumClass = method.parameterTypes[2]
val enumConstants = enumClass.enumConstants ?: enumClass.getMethod("values").invoke(null) as? Array<*>
?: return@runCatching context.log.error("Could not get enum constants")
val addedByUsername = enumConstants.firstOrNull { it.toString().contains("USERNAME", ignoreCase = true) }
?: return@runCatching context.log.error("Could not find ADDED_BY_USERNAME enum")
val sourceTypeDefault = findStaticField(sourceTypeClass) ?: return@runCatching context.log.error("Could not find source type static field")
val pageTypeDefault = findStaticField(pageTypeClass) ?: return@runCatching context.log.error("Could not find page type static field")
method.isAccessible = true
val paramCount = method.parameterTypes.size
val args = arrayOfNulls<Any?>(paramCount).apply {
if (paramCount >= 1) set(0, friendRelationshipChangerInstance)
if (paramCount >= 2) set(1, userId)
if (paramCount >= 3) set(2, addedByUsername)
if (paramCount >= 4) set(3, sourceTypeDefault)
if (paramCount >= 5) set(4, pageTypeDefault)
for (i in 5 until paramCount - 1) set(i, null)
if (paramCount >= 14) set(13, 4064)
else if (paramCount >= 13) set(paramCount - 1, 4064)
}
val result = method.invoke(null, *args)
result?.javaClass?.methods?.firstOrNull { it.name == "subscribe" && it.parameterCount == 0 }?.let { it.isAccessible = true; it.invoke(result) }
}.onFailure {
context.log.error("Failed to accept friend request $userId", it)
}
}
}
private fun ignoreFriendRequest(userId: String) {
context.database.setIncomingRequestIgnored(userId)
}
private suspend fun cleanConversation( private suspend fun cleanConversation(
conversationId: String, conversationId: String,
setDialogMessage: (String) -> Unit setDialogMessage: (String) -> Unit

View File

@@ -487,6 +487,39 @@ class DatabaseAccess(
} }
} }
/** User IDs in FriendWhoAddedMe with added=0 and ignored=0 (real pending requests as shown in Snapchat). */
fun getIncomingRequestUserIds(): Set<String> {
return useDatabase(DatabaseType.MAIN)?.performOperation {
safeRawQuery(
"SELECT userId FROM FriendWhoAddedMe WHERE userId IS NOT NULL AND COALESCE(added, 0) = 0 AND COALESCE(ignored, 0) = 0",
null
)?.use { query ->
val set = mutableSetOf<String>()
while (query.moveToNext()) {
query.getStringOrNull("userId")?.let { set.add(it) }
}
set
}
} ?: emptySet()
}
/** Mark an incoming request as ignored locally (FriendWhoAddedMe.ignored = 1). */
fun setIncomingRequestIgnored(userId: String): Boolean {
var updated = false
useDatabase(DatabaseType.MAIN, writeMode = true)?.apply {
performOperation {
updated = update(
"FriendWhoAddedMe",
ContentValues().apply { put("ignored", 1) },
"userId = ?",
arrayOf(userId)
) > 0
}
close()
}
return updated
}
fun setStoriesViewedState(userId: String, viewed: Boolean): Boolean { fun setStoriesViewedState(userId: String, viewed: Boolean): Boolean {
var success = false var success = false
useDatabase(DatabaseType.MAIN, writeMode = true)?.apply { useDatabase(DatabaseType.MAIN, writeMode = true)?.apply {