From 17b6d2991771a0f148431d2e8093f274277b7392 Mon Sep 17 00:00:00 2001 From: R0rt1z2 Date: Mon, 23 Dec 2024 02:55:37 +0100 Subject: [PATCH] GrindrPlus: Introduce block/unblock notifications --- .../java/com/grindrplus/commands/Profile.kt | 14 ++- .../com/grindrplus/core/DatabaseHelper.kt | 74 ++++++++++++++++ .../main/java/com/grindrplus/core/Utils.kt | 39 ++++++++ .../java/com/grindrplus/core/http/Client.kt | 50 ++++++++--- .../java/com/grindrplus/hooks/AntiBlock.kt | 88 +++++++++++++++++-- 5 files changed, 243 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/grindrplus/core/DatabaseHelper.kt diff --git a/app/src/main/java/com/grindrplus/commands/Profile.kt b/app/src/main/java/com/grindrplus/commands/Profile.kt index 39f26fc..3cd3c7f 100644 --- a/app/src/main/java/com/grindrplus/commands/Profile.kt +++ b/app/src/main/java/com/grindrplus/commands/Profile.kt @@ -30,15 +30,18 @@ class Profile( @Command("block", help = "Block a user") fun block(args: List) { GrindrPlus.httpClient.blockUser( - if (args.isNotEmpty()) args[0] else sender) + if (args.isNotEmpty()) args[0] else sender, + silent = args.contains("silent"), + reflectInDb = !args.contains("no-reflect") + ) } @Command("clear", aliases = ["reset"], help = "Reset chat with a user") fun reset(args: List) { val profileId = if (args.isNotEmpty()) args[0] else sender - block(listOf(profileId, "silent")) + block(listOf(profileId, "silent", "no-reflect")) Thread.sleep(300) - unblock(listOf(profileId, "silent")) + unblock(listOf(profileId, "silent", "no-reflect")) Thread.sleep(300) openChat("$recipient:$profileId") } @@ -46,7 +49,10 @@ class Profile( @Command("unblock", help = "Unblock a user") fun unblock(args: List) { GrindrPlus.httpClient.unblockUser( - if (args.isNotEmpty()) args[0] else sender) + if (args.isNotEmpty()) args[0] else sender, + silent = args.contains("silent"), + reflectInDb = !args.contains("no-reflect") + ) } @Command("chat", help = "Open chat with a user") diff --git a/app/src/main/java/com/grindrplus/core/DatabaseHelper.kt b/app/src/main/java/com/grindrplus/core/DatabaseHelper.kt new file mode 100644 index 0000000..4e1cdd5 --- /dev/null +++ b/app/src/main/java/com/grindrplus/core/DatabaseHelper.kt @@ -0,0 +1,74 @@ +package com.grindrplus.core + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import com.grindrplus.GrindrPlus + +object DatabaseHelper { + private fun getDatabase(): SQLiteDatabase { + val context = GrindrPlus.context + val databases = context.databaseList() + val grindrUserDb = databases.firstOrNull { it.contains("grindr_user") } + ?: throw IllegalStateException("No database matching 'grindr_user' found") + return context.openOrCreateDatabase(grindrUserDb, Context.MODE_PRIVATE, null) + } + + fun query(query: String, args: Array? = null): List> { + val database = getDatabase() + val cursor = database.rawQuery(query, args) + val results = mutableListOf>() + + try { + if (cursor.moveToFirst()) { + do { + val row = mutableMapOf() + cursor.columnNames.forEach { column -> + row[column] = when (cursor.getType(cursor.getColumnIndexOrThrow(column))) { + Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(cursor.getColumnIndexOrThrow(column)) + Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(cursor.getColumnIndexOrThrow(column)) + Cursor.FIELD_TYPE_STRING -> cursor.getString(cursor.getColumnIndexOrThrow(column)) + Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(cursor.getColumnIndexOrThrow(column)) + Cursor.FIELD_TYPE_NULL -> "NULL" + else -> "UNKNOWN" + } + } + results.add(row) + } while (cursor.moveToNext()) + } + } finally { + cursor.close() + database.close() + } + + return results + } + + fun insert(table: String, values: ContentValues): Long { + val database = getDatabase() + val rowId = database.insert(table, null, values) + database.close() + return rowId + } + + fun update(table: String, values: ContentValues, whereClause: String?, whereArgs: Array?): Int { + val database = getDatabase() + val rowsAffected = database.update(table, values, whereClause, whereArgs) + database.close() + return rowsAffected + } + + fun delete(table: String, whereClause: String?, whereArgs: Array?): Int { + val database = getDatabase() + val rowsDeleted = database.delete(table, whereClause, whereArgs) + database.close() + return rowsDeleted + } + + fun execute(sql: String) { + val database = getDatabase() + database.execSQL(sql) + database.close() + } +} diff --git a/app/src/main/java/com/grindrplus/core/Utils.kt b/app/src/main/java/com/grindrplus/core/Utils.kt index 28f7962..402ad1c 100644 --- a/app/src/main/java/com/grindrplus/core/Utils.kt +++ b/app/src/main/java/com/grindrplus/core/Utils.kt @@ -1,8 +1,15 @@ package com.grindrplus.core +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import com.grindrplus.GrindrPlus +import com.grindrplus.ui.Utils.getId import com.grindrplus.utils.RetrofitUtils import java.lang.reflect.Proxy import kotlin.math.pow @@ -186,4 +193,36 @@ object Utils { null } } + + @SuppressLint("MissingPermission", "NotificationPermission") + fun sendNotification( + context: Context, + title: String, + message: String, + notificationId: Int, + channelId: String = "default_channel_id", + channelName: String = "Default Channel", + channelDescription: String = "Default notifications" + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(channelId, channelName, importance).apply { + description = channelDescription + } + val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + + val notificationBuilder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(getId("applovin_ic_warning","drawable", context)) + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + + with(NotificationManagerCompat.from(context)) { + notify(notificationId, notificationBuilder.build()) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/grindrplus/core/http/Client.kt b/app/src/main/java/com/grindrplus/core/http/Client.kt index a6c29b3..1e2b790 100644 --- a/app/src/main/java/com/grindrplus/core/http/Client.kt +++ b/app/src/main/java/com/grindrplus/core/http/Client.kt @@ -1,8 +1,10 @@ package com.grindrplus.core.http +import android.content.ContentValues import android.widget.Toast import com.grindrplus.GrindrPlus import com.grindrplus.GrindrPlus.showToast +import com.grindrplus.core.DatabaseHelper import okhttp3.* import okhttp3.RequestBody.Companion.toRequestBody @@ -36,36 +38,60 @@ class Client(interceptor: Interceptor) { return httpClient.newCall(requestBuilder.build()).execute() } - fun blockUser(profileId: String) { + fun blockUser(profileId: String, silent: Boolean = false, reflectInDb: Boolean = true) { GrindrPlus.executeAsync { val response = sendRequest( "https://grindr.mobi/v3/me/blocks/$profileId", "POST" ) if (response.isSuccessful) { - showToast(Toast.LENGTH_LONG, "User blocked successfully") + if (!silent) showToast(Toast.LENGTH_LONG, "User blocked successfully") + if (reflectInDb) { + val order = DatabaseHelper.query( + "SELECT MAX(order_) AS order_ FROM blocks" + ).firstOrNull()?.get("order_") as? Int ?: 0 + GrindrPlus.logger.log("Adding user $profileId to block list with order ${order + 1}") + DatabaseHelper.insert( + "blocks", + ContentValues().apply { + put("profileId", profileId) + put("order_", order + 1) + } + ) + } } else { - showToast( - Toast.LENGTH_LONG, - "Failed to block user: ${response.body?.string()}" - ) + if (!silent) { + showToast( + Toast.LENGTH_LONG, + "Failed to block user: ${response.body?.string()}" + ) + } } } } - fun unblockUser(profileId: String) { + fun unblockUser(profileId: String, silent: Boolean = false, reflectInDb: Boolean = true) { GrindrPlus.executeAsync { val response = sendRequest( "https://grindr.mobi/v3/me/blocks/$profileId", "DELETE" ) if (response.isSuccessful) { - showToast(Toast.LENGTH_LONG, "User unblocked successfully") + if (!silent) showToast(Toast.LENGTH_LONG, "User unblocked successfully") + if (reflectInDb) { + DatabaseHelper.delete( + "blocks", + "profileId = ?", + arrayOf(profileId) + ) + } } else { - showToast( - Toast.LENGTH_LONG, - "Failed to unblock user: ${response.body?.string()}" - ) + if (!silent) { + showToast( + Toast.LENGTH_LONG, + "Failed to unblock user: ${response.body?.string()}" + ) + } } } } diff --git a/app/src/main/java/com/grindrplus/hooks/AntiBlock.kt b/app/src/main/java/com/grindrplus/hooks/AntiBlock.kt index 1055b45..4ef1d49 100644 --- a/app/src/main/java/com/grindrplus/hooks/AntiBlock.kt +++ b/app/src/main/java/com/grindrplus/hooks/AntiBlock.kt @@ -3,23 +3,99 @@ package com.grindrplus.hooks import android.os.Build import androidx.annotation.RequiresApi import com.grindrplus.GrindrPlus +import com.grindrplus.GrindrPlus.instanceManager +import com.grindrplus.core.DatabaseHelper +import com.grindrplus.core.Utils.sendNotification import com.grindrplus.utils.Hook import com.grindrplus.utils.HookStage -import com.grindrplus.utils.hook import com.grindrplus.utils.hookConstructor -import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers.getObjectField +import org.json.JSONObject class AntiBlock : Hook( "Anti Block", "Alerts you when people block you (prevents the chat from clearing)" ) { - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private var myProfileId: Long = 0 + override fun init() { findClass("com.grindrapp.android.chat.model.ConversationDeleteNotification") - .hookConstructor(HookStage.BEFORE) { param -> - @Suppress("UNCHECKED_CAST") + .hookConstructor(HookStage.AFTER) { param -> + if (myProfileId == 0L) { + myProfileId = (getObjectField(instanceManager + .getInstance("com.grindrapp.android.storage.b"), + "p") as String).toLong() + } val profiles = param.args().firstOrNull() as? List ?: emptyList() - param.setArg(0, emptyList()) + val conversationId = profiles.joinToString(",") + val conversationIds = conversationId.split(":").mapNotNull { it.toLongOrNull() } + val otherProfileId = conversationIds.firstOrNull { it != myProfileId } ?: return@hookConstructor + + // FIXME: Get rid of this ugly shit + if (otherProfileId == myProfileId) return@hookConstructor + Thread.sleep(300) + + if (DatabaseHelper.query( + "SELECT * FROM blocks WHERE profileId = ?", + arrayOf(otherProfileId.toString()) + ).isNotEmpty() + ) { + return@hookConstructor + } + + val response = fetchProfileData(otherProfileId.toString()) + handleProfileResponse(otherProfileId, response) + } + } + + private fun fetchProfileData(profileId: String): String { + return try { + val response = GrindrPlus.httpClient.sendRequest( + url = "https://grindr.mobi/v4/profiles/$profileId", + method = "GET" + ) + + if (response.isSuccessful) { + response.body?.string() ?: "Empty response" + } else { + "Error: ${response.code} - ${response.message}" + } + } catch (e: Exception) { + "Error fetching profile: ${e.message}" + } + } + + private fun handleProfileResponse(profileId: Long, response: String) { + try { + val jsonResponse = JSONObject(response) + val profilesArray = jsonResponse.optJSONArray("profiles") + + if (profilesArray == null || profilesArray.length() == 0) { + val name = (DatabaseHelper.query( + "SELECT name FROM chat_conversations WHERE conversation_id = ?", + arrayOf(profileId.toString()) + ).firstOrNull()?.get("name") as? String)?.takeIf { + name -> name.isNotEmpty() } ?: profileId.toString() + GrindrPlus.logger.log("User $name has blocked you") + sendNotification( + GrindrPlus.context, + "Blocked by User", + "You have been blocked by user $name", + profileId.toInt() + ) + } else { + val profile = profilesArray.getJSONObject(0) + val displayName = profile.optString("displayName", profileId.toString()) + GrindrPlus.logger.log("User $profileId (Name: $displayName) unblocked you") + sendNotification( + GrindrPlus.context, + "Unblocked by $displayName", + "$displayName has unblocked you.", + profileId.toInt(), + ) } + } catch (e: Exception) { + GrindrPlus.logger.log("Error handling profile response: ${e.message}") + } } }