feat: half swipe notifier

This commit is contained in:
rhunk
2023-11-11 14:15:58 +01:00
parent a568b9c1c6
commit dc30d4ee25
6 changed files with 143 additions and 2 deletions

View File

@ -23,10 +23,11 @@ class ConfigurationOverride : Feature("Configuration Override", loadParams = Fea
fun getConfigKeyInfo(key: Any?) = runCatching {
if (key == null) return@runCatching null
val keyClassMethods = key::class.java.methods
val keyName = keyClassMethods.firstOrNull { it.name == "getName" }?.invoke(key)?.toString() ?: key.toString()
val category = keyClassMethods.firstOrNull { it.name == enumMappings["getCategory"].toString() }?.invoke(key)?.toString() ?: return null
val valueHolder = keyClassMethods.firstOrNull { it.name == enumMappings["getValue"].toString() }?.invoke(key) ?: return null
val defaultValue = valueHolder.getObjectField(enumMappings["defaultValueField"].toString()) ?: return null
ConfigKeyInfo(category, key.toString(), defaultValue)
ConfigKeyInfo(category, keyName, defaultValue)
}.onFailure {
context.log.error("Failed to get config key info", it)
}.getOrNull()

View File

@ -117,7 +117,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
private fun setupNotificationActionButtons(contentType: ContentType, conversationId: String, message: Message, notificationData: NotificationData) {
val actions = mutableListOf<Notification.Action>()
actions.addAll(notificationData.notification.actions)
actions.addAll(notificationData.notification.actions ?: emptyArray())
fun newAction(title: String, remoteAction: String, filter: (() -> Boolean), builder: (Notification.Action.Builder) -> Unit) {
if (!filter()) return

View File

@ -0,0 +1,127 @@
package me.rhunk.snapenhance.core.features.impl.spying
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import me.rhunk.snapenhance.common.Constants
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
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
import me.rhunk.snapenhance.core.util.ktx.getObjectField
import java.util.concurrent.ConcurrentHashMap
import kotlin.time.Duration.Companion.milliseconds
class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoadParams.INIT_SYNC) {
private val peekingConversations = ConcurrentHashMap<String, List<String>>()
private val startPeekingTimestamps = ConcurrentHashMap<String, Long>()
private val svgEyeDrawable by lazy { context.resources.getIdentifier("svg_eye_24x24", "drawable") }
private val notificationManager get() = context.androidContext.getSystemService(NotificationManager::class.java)
private val translation by lazy { context.translation.getCategory("half_swipe_notifier")}
private val channelId by lazy {
"peeking".also {
notificationManager.createNotificationChannel(
NotificationChannel(
it,
translation["notification_channel_name"],
NotificationManager.IMPORTANCE_HIGH
)
)
}
}
override fun init() {
if (!context.config.messaging.halfSwipeNotifier.get()) return
lateinit var presenceService: Any
findClass("com.snapchat.talkcorev3.PresenceService\$CppProxy").hookConstructor(HookStage.AFTER) {
presenceService = it.thisObject()
}
PendingIntent::class.java.methods.find { it.name == "getActivity" }?.hook(HookStage.BEFORE) { param ->
context.log.verbose(param.args().toList())
}
context.mappings.getMappedClass("callbacks", "PresenceServiceDelegate")
.hook("notifyActiveConversationsChanged", HookStage.BEFORE) {
val activeConversations = presenceService::class.java.methods.find { it.name == "getActiveConversations" }?.invoke(presenceService) as? Map<*, *> ?: return@hook // conversationId, conversationInfo (this.mPeekingParticipants)
if (activeConversations.isEmpty()) {
peekingConversations.forEach {
val conversationId = it.key
val peekingParticipantsIds = it.value
peekingParticipantsIds.forEach { userId ->
endPeeking(conversationId, userId)
}
}
peekingConversations.clear()
return@hook
}
activeConversations.forEach { (conversationId, conversationInfo) ->
val peekingParticipantsIds = (conversationInfo?.getObjectField("mPeekingParticipants") as? List<*>)?.map { it.toString() } ?: return@forEach
val cachedPeekingParticipantsIds = peekingConversations[conversationId] ?: emptyList()
val newPeekingParticipantsIds = peekingParticipantsIds - cachedPeekingParticipantsIds.toSet()
val exitedPeekingParticipantsIds = cachedPeekingParticipantsIds - peekingParticipantsIds.toSet()
newPeekingParticipantsIds.forEach { userId ->
startPeeking(conversationId.toString(), userId)
}
exitedPeekingParticipantsIds.forEach { userId ->
endPeeking(conversationId.toString(), userId)
}
peekingConversations[conversationId.toString()] = peekingParticipantsIds
}
}
}
private fun startPeeking(conversationId: String, userId: String) {
startPeekingTimestamps[conversationId + userId] = System.currentTimeMillis()
}
private fun endPeeking(conversationId: String, userId: String) {
startPeekingTimestamps[conversationId + userId]?.let { startPeekingTimestamp ->
val peekingDuration = (System.currentTimeMillis() - startPeekingTimestamp).milliseconds.inWholeSeconds.toString()
val groupName = context.database.getFeedEntryByConversationId(conversationId)?.feedDisplayName
val friendInfo = context.database.getFriendInfo(userId) ?: return
Notification.Builder(context.androidContext, channelId)
.setContentTitle(groupName ?: friendInfo.displayName ?: friendInfo.mutableUsername)
.setContentText(if (groupName != null) {
translation.format("notification_content_group",
"friend" to (friendInfo.displayName ?: friendInfo.mutableUsername).toString(),
"group" to groupName,
"duration" to peekingDuration
)
} else {
translation.format("notification_content_dm",
"friend" to (friendInfo.displayName ?: friendInfo.mutableUsername).toString(),
"duration" to peekingDuration
)
})
.setContentIntent(
context.androidContext.packageManager.getLaunchIntentForPackage(
Constants.SNAPCHAT_PACKAGE_NAME
)?.let {
PendingIntent.getActivity(
context.androidContext,
0, it, PendingIntent.FLAG_IMMUTABLE
)
}
)
.setAutoCancel(true)
.setSmallIcon(svgEyeDrawable)
.build()
.let { notification ->
notificationManager.notify(System.nanoTime().toInt(), notification)
}
}
}
}

View File

@ -15,6 +15,7 @@ import me.rhunk.snapenhance.core.features.impl.experiments.*
import me.rhunk.snapenhance.core.features.impl.global.*
import me.rhunk.snapenhance.core.features.impl.messaging.*
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.core.features.impl.spying.HalfSwipeNotifier
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
import me.rhunk.snapenhance.core.features.impl.tweaks.CameraTweaks
import me.rhunk.snapenhance.core.features.impl.tweaks.BypassScreenshotDetection
@ -105,6 +106,7 @@ class FeatureManager(
SnapPreview::class,
InstantDelete::class,
BypassScreenshotDetection::class,
HalfSwipeNotifier::class,
)
initializeFeatures()