commit dc30d4ee254581ba86d69742f5ae5d33cbefcc94
parent a568b9c1c615d1d09d232bce960adc4fc747ebb7
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Sat, 11 Nov 2023 14:15:58 +0100
feat: half swipe notifier
Diffstat:
6 files changed, 144 insertions(+), 2 deletions(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -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",
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt
@@ -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",
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt
@@ -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()
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt
@@ -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
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt
@@ -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)
+ }
+ }
+ }
+}+
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt
@@ -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()