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

@ -344,6 +344,10 @@
"name": "Disable Replay in FF",
"description": "Disables the ability to replay with a long press from the Friend Feed"
},
"half_swipe_notifier": {
"name": "Half Swipe Notifier",
"description": "Notifies you when someone half swipes into a conversation"
},
"message_preview_length": {
"name": "Message Preview Length",
"description": "Specify the amount of messages to get previewed"
@ -865,6 +869,12 @@
"dialog_message": "Are you sure you want to start a call?"
},
"half_swipe_notifier": {
"notification_channel_name": "Half Swipe",
"notification_content_dm": "{friend} just half-swiped into your chat for {duration} seconds",
"notification_content_group": "{friend} just half-swiped into {group} for {duration} seconds"
},
"download_processor": {
"attachment_type": {
"snap": "Snap",

View File

@ -12,6 +12,7 @@ class MessagingTweaks : ConfigContainer() {
val hideTypingNotifications = boolean("hide_typing_notifications")
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
val disableReplayInFF = boolean("disable_replay_in_ff")
val halfSwipeNotifier = boolean("half_swipe_notifier") { requireRestart() }
val messagePreviewLength = integer("message_preview_length", defaultValue = 20)
val callStartConfirmation = boolean("call_start_confirmation") { requireRestart() }
val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations",

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()