Friend request
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user