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:
Mcommon/src/main/assets/lang/en_US.json | 10++++++++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ConfigurationOverride.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt | 2+-
Acore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/spying/HalfSwipeNotifier.kt | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt | 2++
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()