Skip to content

Commit

Permalink
GrindrPlus: Introduce block/unblock notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
R0rt1z2 committed Dec 23, 2024
1 parent a843750 commit 17b6d29
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 22 deletions.
14 changes: 10 additions & 4 deletions app/src/main/java/com/grindrplus/commands/Profile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,29 @@ class Profile(
@Command("block", help = "Block a user")
fun block(args: List<String>) {
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<String>) {
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")
}

@Command("unblock", help = "Unblock a user")
fun unblock(args: List<String>) {
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")
Expand Down
74 changes: 74 additions & 0 deletions app/src/main/java/com/grindrplus/core/DatabaseHelper.kt
Original file line number Diff line number Diff line change
@@ -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<String>? = null): List<Map<String, Any>> {
val database = getDatabase()
val cursor = database.rawQuery(query, args)
val results = mutableListOf<Map<String, Any>>()

try {
if (cursor.moveToFirst()) {
do {
val row = mutableMapOf<String, Any>()
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<String>?): Int {
val database = getDatabase()
val rowsAffected = database.update(table, values, whereClause, whereArgs)
database.close()
return rowsAffected
}

fun delete(table: String, whereClause: String?, whereArgs: Array<String>?): 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()
}
}
39 changes: 39 additions & 0 deletions app/src/main/java/com/grindrplus/core/Utils.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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())
}
}
}
50 changes: 38 additions & 12 deletions app/src/main/java/com/grindrplus/core/http/Client.kt
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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()}"
)
}
}
}
}
Expand Down
88 changes: 82 additions & 6 deletions app/src/main/java/com/grindrplus/hooks/AntiBlock.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> ?: emptyList()
param.setArg(0, emptyList<String>())
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}")
}
}
}

0 comments on commit 17b6d29

Please sign in to comment.