feat: wrong architecture detector
This commit is contained in:
@@ -3,6 +3,7 @@ package me.eternal.purrfectsnap.ui.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -10,6 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -22,6 +24,7 @@ import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
@@ -33,11 +36,14 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import me.eternal.purrfectsnap.RemoteSideContext
|
||||
import me.eternal.purrfectsnap.SharedContextHolder
|
||||
import me.eternal.purrfectsnap.common.ui.AppMaterialTheme
|
||||
import me.eternal.purrfectsnap.common.ui.ThemeMode
|
||||
import me.eternal.purrfectsnap.common.ui.ThemePreferences
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.ActivityLauncherHelper
|
||||
import me.eternal.purrfectsnap.ui.util.ThankYouDialog
|
||||
@@ -113,6 +119,31 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
AppMaterialTheme(themeMode = themeMode) {
|
||||
val shouldShowAbiWarning = remember {
|
||||
val deviceIsArm64 = Build.SUPPORTED_ABIS.any { it == "arm64-v8a" || it.startsWith("arm64") }
|
||||
val libDir = context.applicationInfo.nativeLibraryDir.orEmpty()
|
||||
val appIsArm64 = libDir.contains("arm64")
|
||||
deviceIsArm64 && !appIsArm64
|
||||
}
|
||||
if (shouldShowAbiWarning) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = {},
|
||||
title = "Wrong APK installed",
|
||||
text = "",
|
||||
icon = Icons.Filled.Warning,
|
||||
confirmButtonText = "Close",
|
||||
onConfirm = { (context as? Activity)?.finishAffinity() },
|
||||
showCloseButton = false,
|
||||
opaque = true,
|
||||
customContent = {
|
||||
Text(
|
||||
text = "Your device is armv8, please download the armv8 apk, not armv7.",
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
val background = MaterialTheme.colorScheme.background
|
||||
val isLight = background.luminance() > 0.5f
|
||||
val view = LocalView.current
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package me.eternal.purrfectsnap.ui.setup
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
@@ -78,6 +79,7 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -203,6 +205,7 @@ class SetupActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
setContent {
|
||||
val context = LocalContext.current
|
||||
val navController = rememberNavController()
|
||||
var canGoNext by remember { mutableStateOf(false) }
|
||||
var lastRoute by rememberSaveable { mutableStateOf("") }
|
||||
@@ -214,6 +217,31 @@ class SetupActivity : ComponentActivity() {
|
||||
}
|
||||
val skipPatch by rememberSaveable { skipPatchChoice }
|
||||
val installMode by installModeChoice
|
||||
val shouldShowAbiWarning = remember {
|
||||
val deviceIsArm64 = Build.SUPPORTED_ABIS.any { it == "arm64-v8a" || it.startsWith("arm64") }
|
||||
val libDir = context.applicationInfo.nativeLibraryDir.orEmpty()
|
||||
val appIsArm64 = libDir.contains("arm64")
|
||||
deviceIsArm64 && !appIsArm64
|
||||
}
|
||||
if (shouldShowAbiWarning) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = {},
|
||||
title = "Wrong APK installed",
|
||||
text = "",
|
||||
icon = Icons.Filled.Warning,
|
||||
confirmButtonText = "Close",
|
||||
onConfirm = { (context as? Activity)?.finishAffinity() },
|
||||
showCloseButton = false,
|
||||
opaque = true,
|
||||
customContent = {
|
||||
Text(
|
||||
text = "Your device is armv8, please download the armv8 apk, not armv7.",
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
val visibleScreens = remember(skipPatch, installMode) {
|
||||
requiredScreens.filterNot { screen ->
|
||||
if (skipPatch && (screen is PatchSnapchatScreen || screen is RootInstallSnapchatScreen)) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package me.eternal.purrfectsnap.core.features.impl.messaging
|
||||
|
||||
import me.eternal.purrfectsnap.common.data.ContentType
|
||||
import me.eternal.purrfectsnap.common.data.NotificationType
|
||||
import me.eternal.purrfectsnap.common.util.protobuf.ProtoEditor
|
||||
import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.NativeUnaryCallEvent
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.UnaryCallEvent
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent
|
||||
@@ -26,12 +28,41 @@ class PreventMessageSending : Feature("Prevent message sending") {
|
||||
}.toByteArray()
|
||||
}
|
||||
|
||||
context.event.subscribe(NativeUnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event ->
|
||||
handleUpdateContentMessage(event.uri, event.buffer)?.let { event.buffer = it }
|
||||
fun handleNativeUnaryCall(uri: String, buffer: ByteArray): ByteArray? {
|
||||
if (uri == "/messagingcoreservice.MessagingCoreService/UpdateContentMessage") {
|
||||
return handleUpdateContentMessage(uri, buffer)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
context.event.subscribe(UnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event ->
|
||||
handleUpdateContentMessage(event.uri, event.buffer)?.let { event.buffer = it }
|
||||
context.event.subscribe(NativeUnaryCallEvent::class) { event ->
|
||||
if (event.uri == "/messagingcoreservice.MessagingCoreService/CreateContentMessage") {
|
||||
val contentTypeId = ProtoReader(event.buffer).getVarInt(4, 2)?.toInt() ?: return@subscribe
|
||||
val associatedType = NotificationType.fromContentType(ContentType.fromId(contentTypeId)) ?: return@subscribe
|
||||
if (preventMessageSending.contains(associatedType.key) && associatedType.key != "snap_replay") {
|
||||
context.log.verbose("Preventing native CreateContentMessage for $associatedType")
|
||||
event.canceled = true
|
||||
}
|
||||
}
|
||||
|
||||
if (preventMessageSending.contains("snap_replay")) {
|
||||
handleNativeUnaryCall(event.uri, event.buffer)?.let { event.buffer = it }
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(UnaryCallEvent::class) { event ->
|
||||
if (event.uri == "/messagingcoreservice.MessagingCoreService/CreateContentMessage") {
|
||||
val contentTypeId = ProtoReader(event.buffer).getVarInt(4, 2)?.toInt() ?: return@subscribe
|
||||
val associatedType = NotificationType.fromContentType(ContentType.fromId(contentTypeId)) ?: return@subscribe
|
||||
if (preventMessageSending.contains(associatedType.key) && associatedType.key != "snap_replay") {
|
||||
context.log.verbose("Preventing CreateContentMessage for $associatedType")
|
||||
event.canceled = true
|
||||
}
|
||||
}
|
||||
|
||||
if (preventMessageSending.contains("snap_replay")) {
|
||||
handleNativeUnaryCall(event.uri, event.buffer)?.let { event.buffer = it }
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.hook("updateMessage", HookStage.BEFORE) { param ->
|
||||
|
||||
@@ -3,12 +3,8 @@ package me.eternal.purrfectsnap.core.features.impl.messaging
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
@@ -16,11 +12,9 @@ import androidx.compose.runtime.*
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -38,10 +32,7 @@ import me.eternal.purrfectsnap.core.event.events.impl.NativeUnaryCallEvent
|
||||
import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent
|
||||
import me.eternal.purrfectsnap.core.features.Feature
|
||||
import me.eternal.purrfectsnap.core.features.impl.experiments.MediaFilePicker
|
||||
import me.eternal.purrfectsnap.core.features.impl.tweaks.UnsaveableMessages
|
||||
import me.eternal.purrfectsnap.core.messaging.MessageSender
|
||||
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayPalette
|
||||
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme
|
||||
import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull
|
||||
import me.eternal.purrfectsnap.core.util.hook.HookStage
|
||||
import me.eternal.purrfectsnap.core.util.hook.hook
|
||||
@@ -154,8 +145,7 @@ class SendOverride : Feature("Send Override") {
|
||||
val stripMediaMetadata = context.config.messaging.stripMediaMetadata.get()
|
||||
var postSavePolicy: Int? = null
|
||||
|
||||
val configOverrideType = context.config.messaging.galleryMediaSendOverride.mode.getNullable()
|
||||
val includeCameraSnaps = context.config.messaging.galleryMediaSendOverride.includeCameraSnaps.get()
|
||||
val configOverrideType = context.config.messaging.galleryMediaSendOverride.mode.getNullable()?.toString()
|
||||
if (configOverrideType == null && stripMediaMetadata.isEmpty()) return
|
||||
|
||||
context.event.subscribe(MediaUploadEvent::class) { event ->
|
||||
@@ -186,30 +176,21 @@ class SendOverride : Feature("Send Override") {
|
||||
}
|
||||
}
|
||||
if (stripMediaMetadata.contains("remove_audio_note_transcript_capability")) {
|
||||
// Remove locale string (field 3) which may be used for transcript capability
|
||||
// Also try other potential fields
|
||||
if (hasFullPath) {
|
||||
edit(4, 4, 6, 1, 1) {
|
||||
remove(2) // potential transcript-related field
|
||||
remove(3) // locale string (may affect transcript capability)
|
||||
remove(4) // potential transcript-related field
|
||||
}
|
||||
edit(4, 4, 6, 1) {
|
||||
remove(2) // possible transcript capability flag at parent level
|
||||
remove(3) // locale at parent level
|
||||
remove(3) // locale string
|
||||
}
|
||||
}
|
||||
if (hasDirectPath || !hasFullPath) {
|
||||
edit(6, 1, 1) {
|
||||
remove(2) // potential transcript-related field
|
||||
remove(3) // locale string (may affect transcript capability)
|
||||
remove(4) // potential transcript-related field
|
||||
}
|
||||
edit(6, 1) {
|
||||
remove(2) // possible transcript capability flag at parent level
|
||||
remove(3) // locale at parent level (field 3 at 6,1 is locale per MessageSender)
|
||||
remove(3) // locale string
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
val field = result.messageContent.instanceNonNull().javaClass.getDeclaredField("mAllowsTranscription")
|
||||
field.isAccessible = true
|
||||
field.set(result.messageContent.instanceNonNull(), false)
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
@@ -284,27 +265,71 @@ class SendOverride : Feature("Send Override") {
|
||||
|
||||
if (configOverrideType == null) return
|
||||
|
||||
context.event.subscribe(NativeUnaryCallEvent::class) { event ->
|
||||
context.event.subscribe(NativeUnaryCallEvent::class, priority = 100) { event ->
|
||||
if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe
|
||||
postSavePolicy?.let { savePolicy ->
|
||||
context.log.verbose("postSavePolicy=$savePolicy")
|
||||
val protoReader = ProtoReader(event.buffer)
|
||||
event.buffer = ProtoEditor(event.buffer).apply {
|
||||
edit(4) {
|
||||
remove(7)
|
||||
addVarInt(7, savePolicy)
|
||||
}
|
||||
// Handle chat messages (field 4)
|
||||
if (protoReader.followPath(4) != null) {
|
||||
edit(4) {
|
||||
remove(7)
|
||||
addVarInt(7, savePolicy)
|
||||
}
|
||||
|
||||
// remove Keep Snaps in Chat ability
|
||||
if (savePolicy == 1/* PROHIBITED */) {
|
||||
edit(6, 9) {
|
||||
remove(1)
|
||||
// remove Keep Snaps in Chat ability
|
||||
if (savePolicy == 1/* PROHIBITED */) {
|
||||
edit(6, 9) {
|
||||
remove(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle NOTE messages (field 6) - audio notes
|
||||
// Check both root level (6) and nested under media (4, 4, 6)
|
||||
val noteAtRoot = protoReader.followPath(6) != null
|
||||
val noteNested = protoReader.followPath(4, 4, 6) != null
|
||||
|
||||
if (noteAtRoot || noteNested) {
|
||||
// Set save policy in the NOTE path
|
||||
val hasNestedPath = if (noteNested) {
|
||||
protoReader.followPath(4, 4, 6, 1, 1) != null
|
||||
} else {
|
||||
protoReader.followPath(6, 1, 1) != null
|
||||
}
|
||||
|
||||
if (noteNested) {
|
||||
if (hasNestedPath) {
|
||||
edit(4, 4, 6, 1, 1) {
|
||||
remove(7)
|
||||
addVarInt(7, savePolicy)
|
||||
}
|
||||
} else {
|
||||
edit(4, 4, 6, 1) {
|
||||
remove(7)
|
||||
addVarInt(7, savePolicy)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (hasNestedPath) {
|
||||
edit(6, 1, 1) {
|
||||
remove(7)
|
||||
addVarInt(7, savePolicy)
|
||||
}
|
||||
} else {
|
||||
edit(6, 1) {
|
||||
remove(7)
|
||||
addVarInt(7, savePolicy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
|
||||
context.event.subscribe(SendMessageWithContentEvent::class, priority = -100) { event ->
|
||||
postSavePolicy = null
|
||||
if (event.destinations.stories?.isNotEmpty() == true && event.destinations.conversations?.isEmpty() == true) return@subscribe
|
||||
val localMessageContent = event.messageContent
|
||||
@@ -312,9 +337,6 @@ class SendOverride : Feature("Send Override") {
|
||||
if (localMessageContent.contentType != ContentType.EXTERNAL_MEDIA &&
|
||||
localMessageContent.contentType != ContentType.SNAP &&
|
||||
localMessageContent.instanceNonNull().getObjectFieldOrNull("mExternalContentMetadata") == null) return@subscribe
|
||||
val isCameraSnap = localMessageContent.contentType == ContentType.SNAP &&
|
||||
localMessageContent.instanceNonNull().getObjectFieldOrNull("mExternalContentMetadata") == null
|
||||
if (isCameraSnap && !includeCameraSnaps) return@subscribe
|
||||
|
||||
//prevent story replies
|
||||
val messageProtoReader = ProtoReader(localMessageContent.content ?: return@subscribe)
|
||||
@@ -339,7 +361,8 @@ class SendOverride : Feature("Send Override") {
|
||||
event.canceled = true
|
||||
|
||||
fun sendMedia(overrideType: String, snapDurationMs: Int?): Boolean {
|
||||
if (overrideType != "ORIGINAL" && (messageProtoReader.followPath(3)?.getCount(3) ?: 0) > 1) {
|
||||
val bypassLimit = context.config.experimental.nativeHooks.valdiHooks.bypassCameraRollLimit.get()
|
||||
if (overrideType != "ORIGINAL" && !bypassLimit && (messageProtoReader.followPath(3)?.getCount(3) ?: 0) > 1) {
|
||||
context.inAppOverlay.showStatusToast(
|
||||
icon = Icons.Default.WarningAmber,
|
||||
context.translation["gallery_media_send_override.multiple_media_toast"]
|
||||
@@ -349,7 +372,7 @@ class SendOverride : Feature("Send Override") {
|
||||
|
||||
when (overrideType) {
|
||||
"SNAP", "SAVEABLE_SNAP" -> {
|
||||
val savePolicyValue = if (overrideType == "SAVEABLE_SNAP") 0 else 1
|
||||
val savePolicyValue = if (overrideType == "SAVEABLE_SNAP") 2 else 1
|
||||
postSavePolicy = savePolicyValue
|
||||
|
||||
val extras = messageProtoReader.followPath(3, 3, 13)?.getBuffer()
|
||||
@@ -402,6 +425,13 @@ class SendOverride : Feature("Send Override") {
|
||||
}
|
||||
addVarInt(7, savePolicyValue)
|
||||
}
|
||||
// also set at root of snap doc
|
||||
edit(11) {
|
||||
if (getOrNull(7) != null) {
|
||||
remove(7)
|
||||
}
|
||||
addVarInt(7, savePolicyValue)
|
||||
}
|
||||
}.toByteArray()
|
||||
|
||||
try {
|
||||
@@ -433,21 +463,78 @@ class SendOverride : Feature("Send Override") {
|
||||
}
|
||||
}
|
||||
"NOTE" -> {
|
||||
// Check if "prevent audio" is enabled in UnsaveableMessages
|
||||
val shouldPreventSave = context.config.messaging.unsaveableMessages.note.get()
|
||||
if (shouldPreventSave) {
|
||||
postSavePolicy = 1 // PROHIBITED
|
||||
}
|
||||
localMessageContent.contentType = ContentType.NOTE
|
||||
localMessageContent.content =
|
||||
MessageSender.audioNoteProto(
|
||||
messageProtoReader.getVarInt(3, 3, 5, 1, 1, 15) ?: context.feature(MediaFilePicker::class).lastMediaDuration ?: 0,
|
||||
Locale.getDefault().toLanguageTag()
|
||||
)
|
||||
val audioNoteProto = MessageSender.audioNoteProto(
|
||||
messageProtoReader.getVarInt(3, 3, 5, 1, 1, 15) ?: context.feature(MediaFilePicker::class).lastMediaDuration ?: 0,
|
||||
Locale.getDefault().toLanguageTag()
|
||||
)
|
||||
|
||||
// Set save policy in the proto if prevent audio is enabled
|
||||
localMessageContent.content = if (shouldPreventSave) {
|
||||
// Check which path structure exists in the audio note proto
|
||||
val protoReader = ProtoReader(audioNoteProto)
|
||||
val hasNestedPath = protoReader.followPath(6, 1, 1) != null
|
||||
|
||||
ProtoEditor(audioNoteProto).apply {
|
||||
// Set save policy to PROHIBITED (1) in the NOTE path
|
||||
if (hasNestedPath) {
|
||||
edit(6, 1, 1) {
|
||||
remove(7)
|
||||
addVarInt(7, 1)
|
||||
}
|
||||
} else {
|
||||
edit(6, 1) {
|
||||
remove(7)
|
||||
addVarInt(7, 1)
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
} else {
|
||||
audioNoteProto
|
||||
}
|
||||
|
||||
// Set mSavePolicy on Java object if prevent audio is enabled
|
||||
if (shouldPreventSave) {
|
||||
try {
|
||||
val savePolicyEnumClass = runCatching {
|
||||
XposedHelpers.findClass("com.snapchat.client.messaging.SavePolicy",
|
||||
localMessageContent.instanceNonNull().javaClass.classLoader)
|
||||
}.getOrNull()
|
||||
|
||||
if (savePolicyEnumClass != null && savePolicyEnumClass.isEnum) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val enumClass = savePolicyEnumClass as Class<out Enum<*>>
|
||||
val prohibitedEnum = runCatching {
|
||||
java.lang.Enum.valueOf(enumClass, "PROHIBITED")
|
||||
}.getOrNull()
|
||||
|
||||
if (prohibitedEnum != null) {
|
||||
val savePolicyField = localMessageContent.instanceNonNull().javaClass.declaredFields
|
||||
.find { it.name == "mSavePolicy" }
|
||||
|
||||
if (savePolicyField != null) {
|
||||
savePolicyField.isAccessible = true
|
||||
XposedHelpers.setObjectField(localMessageContent.instanceNonNull(), "mSavePolicy", prohibitedEnum)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.log.warn("SendOverride: Failed to set mSavePolicy for NOTE: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
val overrideType = configOverrideType ?: return@subscribe
|
||||
if (overrideType != "always_ask") {
|
||||
if (sendMedia(overrideType, 10)) {
|
||||
if (configOverrideType != null && configOverrideType != "always_ask") {
|
||||
if (sendMedia(configOverrideType, 10)) {
|
||||
event.invokeOriginal()
|
||||
}
|
||||
return@subscribe
|
||||
@@ -455,200 +542,154 @@ class SendOverride : Feature("Send Override") {
|
||||
|
||||
context.runOnUiThread {
|
||||
val recipientNameForTask = recipientName
|
||||
val mediaCount = messageProtoReader.followPath(3)?.getCount(3) ?: 0
|
||||
|
||||
createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
|
||||
PurrfectOverlayTheme {
|
||||
val mainTranslation = remember {
|
||||
context.translation.getCategory("send_override_dialog")
|
||||
val mainTranslation = remember {
|
||||
context.translation.getCategory("send_override_dialog")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionTile(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Boolean = false,
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.then(if (!enabled) Modifier.alpha(0.5f) else Modifier),
|
||||
onClick = { if (enabled) onClick() },
|
||||
elevation = if (selected) CardDefaults.elevatedCardElevation(disabledElevation = 3.dp) else CardDefaults.cardElevation(),
|
||||
colors = if (selected) CardDefaults.elevatedCardColors() else CardDefaults.cardColors()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.size(75.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(icon, contentDescription = title, modifier = Modifier
|
||||
.size(32.dp)
|
||||
.padding(4.dp))
|
||||
Text(title, modifier = Modifier.fillMaxWidth(), fontSize = 12.sp, fontWeight = FontWeight.Light, softWrap = true, lineHeight = 14.sp, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
val dialogShape = RoundedCornerShape(24.dp)
|
||||
val dialogSurfaceColor = Color(0xFF2A2452)
|
||||
val border = remember {
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectOverlayPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val translation = remember {
|
||||
context.translation.getCategory("features.options.gallery_media_send_override")
|
||||
}
|
||||
val dialogBackground = remember {
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
Color(0xFF2A2452),
|
||||
Color(0xFF1A143A)
|
||||
)
|
||||
)
|
||||
var scheduleEnabled by remember { mutableStateOf(false) }
|
||||
|
||||
Text(fontSize = 20.sp, fontWeight = FontWeight.Medium, text = "${mainTranslation["send_as"]} ${
|
||||
translation[selectedType]}", modifier = Modifier.padding(5.dp))
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
ActionTile(selected = selectedType == "ORIGINAL", icon = Icons.Filled.Photo, title = translation["ORIGINAL"], enabled = true) {
|
||||
selectedType = "ORIGINAL"
|
||||
}
|
||||
ActionTile(selected = selectedType == "SNAP" || selectedType == "SAVEABLE_SNAP", icon = Icons.Filled.PhotoCamera, title = translation["SNAP"], enabled = true) {
|
||||
selectedType = "SNAP"
|
||||
}
|
||||
ActionTile(selected = selectedType == "NOTE", icon = Icons.Filled.MusicNote, title = translation["NOTE"], enabled = true) {
|
||||
selectedType = "NOTE"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionTile(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Boolean = false,
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = if (selected) 4.dp else 1.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (selected) Color(0xFF3E3478) else Color(0xFF2F2A5B),
|
||||
contentColor = Color.White
|
||||
),
|
||||
border = if (selected) BorderStroke(1.dp, PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.6f)) else null
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 10.dp, vertical = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.size(28.dp),
|
||||
tint = if (selected) PurrfectOverlayPalette.glowSecondary else Color.White.copy(alpha = 0.9f)
|
||||
fun convertDuration(duration: Float) = when {
|
||||
duration in -2f..-1f -> 100
|
||||
duration in -1f..-0f -> 250
|
||||
duration in -0f..1f -> 500
|
||||
duration >= 11f -> null
|
||||
else -> ((duration * 1000).toInt() / 1000) * 1000
|
||||
}
|
||||
|
||||
fun formatTimeText(ms: Long): String {
|
||||
val days = (ms / (24 * 60 * 60 * 1000)).toInt()
|
||||
val hours = ((ms / (60 * 60 * 1000)) % 24).toInt()
|
||||
val minutes = ((ms / (60 * 1000)) % 60).toInt()
|
||||
val seconds = ((ms / 1000) % 60).toInt()
|
||||
return buildString {
|
||||
if (days > 0) append("${days}d ")
|
||||
if (hours > 0 || days > 0) append("${hours}h ")
|
||||
if (minutes > 0 || hours > 0 || days > 0) append("${minutes}m ")
|
||||
append("${seconds}s")
|
||||
}
|
||||
}
|
||||
|
||||
when (selectedType) {
|
||||
"SNAP", "SAVEABLE_SNAP" -> {
|
||||
fun toggleSaveable() {
|
||||
selectedType = if (selectedType == "SAVEABLE_SNAP") "SNAP" else "SAVEABLE_SNAP"
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable {
|
||||
toggleSaveable()
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
){
|
||||
Checkbox(
|
||||
checked = selectedType == "SAVEABLE_SNAP",
|
||||
onCheckedChange = {
|
||||
toggleSaveable()
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(text = mainTranslation["saveable_snap_hint"], lineHeight = 15.sp)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
text = mainTranslation.format("duration",
|
||||
"duration" to (convertDuration(customDuration)?.toDuration(DurationUnit.MILLISECONDS)?.toString(DurationUnit.SECONDS, 2) ?: mainTranslation["unlimited_duration"])
|
||||
)
|
||||
)
|
||||
Slider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
|
||||
softWrap = true,
|
||||
lineHeight = 14.sp,
|
||||
textAlign = TextAlign.Center
|
||||
enabled = selectedType != "SAVEABLE_SNAP",
|
||||
value = customDuration,
|
||||
onValueChange = {
|
||||
customDuration = it
|
||||
},
|
||||
valueRange = -2f..11f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = dialogShape,
|
||||
color = dialogSurfaceColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 18.dp,
|
||||
border = BorderStroke(1.dp, border)
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(dialogBackground, dialogShape)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val translation = remember {
|
||||
context.translation.getCategory("features.options.gallery_media_send_override")
|
||||
Checkbox(
|
||||
checked = scheduleEnabled,
|
||||
onCheckedChange = {
|
||||
scheduleEnabled = it
|
||||
if (!it) scheduledTime = null
|
||||
}
|
||||
var scheduleEnabled by remember { mutableStateOf(false) }
|
||||
|
||||
Text(fontSize = 20.sp, fontWeight = FontWeight.Medium, text = "Send as ${
|
||||
translation[selectedType]}", modifier = Modifier.padding(5.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ActionTile(
|
||||
modifier = Modifier.weight(1f).height(92.dp),
|
||||
selected = selectedType == "ORIGINAL",
|
||||
icon = Icons.Filled.Photo,
|
||||
title = translation["ORIGINAL"]
|
||||
) {
|
||||
selectedType = "ORIGINAL"
|
||||
}
|
||||
ActionTile(
|
||||
modifier = Modifier.weight(1f).height(92.dp),
|
||||
selected = selectedType == "SNAP" || selectedType == "SAVEABLE_SNAP",
|
||||
icon = Icons.Filled.PhotoCamera,
|
||||
title = translation["SNAP"]
|
||||
) {
|
||||
selectedType = "SNAP"
|
||||
}
|
||||
ActionTile(
|
||||
modifier = Modifier.weight(1f).height(92.dp),
|
||||
selected = selectedType == "NOTE",
|
||||
icon = Icons.Filled.MusicNote,
|
||||
title = translation["NOTE"]
|
||||
) {
|
||||
selectedType = "NOTE"
|
||||
}
|
||||
}
|
||||
|
||||
fun convertDuration(duration: Float): Int? {
|
||||
return when {
|
||||
duration in -2f..-1f -> 100
|
||||
duration in -1f..-0f -> 250
|
||||
duration in -0f..1f -> 500
|
||||
duration >= 11f -> null
|
||||
else -> ((duration * 1000).toInt() / 1000) * 1000
|
||||
}
|
||||
}
|
||||
|
||||
when (selectedType) {
|
||||
"SNAP", "SAVEABLE_SNAP" -> {
|
||||
fun toggleSaveable() {
|
||||
selectedType = if (selectedType == "SAVEABLE_SNAP") "SNAP" else "SAVEABLE_SNAP"
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable {
|
||||
toggleSaveable()
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
){
|
||||
Checkbox(
|
||||
checked = selectedType == "SAVEABLE_SNAP",
|
||||
onCheckedChange = {
|
||||
toggleSaveable()
|
||||
}
|
||||
)
|
||||
Text(text = mainTranslation["saveable_snap_hint"], lineHeight = 15.sp)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = mainTranslation.format("duration",
|
||||
"duration" to (convertDuration(customDuration)?.toDuration(DurationUnit.MILLISECONDS)?.toString(DurationUnit.SECONDS, 2) ?: mainTranslation["unlimited_duration"])
|
||||
)
|
||||
)
|
||||
Slider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = selectedType != "SAVEABLE_SNAP",
|
||||
value = customDuration,
|
||||
onValueChange = {
|
||||
customDuration = it
|
||||
},
|
||||
valueRange = -2f..11f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = scheduleEnabled,
|
||||
onCheckedChange = {
|
||||
scheduleEnabled = it
|
||||
if (!it) scheduledTime = null
|
||||
}
|
||||
)
|
||||
Text(text = mainTranslation["schedule"], modifier = Modifier.weight(1f))
|
||||
if (scheduleEnabled) {
|
||||
Button(onClick = { showClockPicker = true }) {
|
||||
scheduledTime?.let { time ->
|
||||
Text(text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(time))
|
||||
} ?: Text(context.translation["select"])
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(text = mainTranslation["schedule"], modifier = Modifier.weight(1f))
|
||||
if (scheduleEnabled) {
|
||||
Button(onClick = { showClockPicker = true }) {
|
||||
scheduledTime?.let { time ->
|
||||
Text(text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(time))
|
||||
} ?: Text(context.translation["select"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scheduleEnabled && showClockPicker) {
|
||||
val datePickerState = rememberDatePickerState(
|
||||
@@ -662,7 +703,6 @@ class SendOverride : Feature("Send Override") {
|
||||
var showDatePickerDialog by remember { mutableStateOf(false) }
|
||||
var showTimePickerDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// Date Picker Dialog
|
||||
if (showDatePickerDialog) {
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { showDatePickerDialog = false },
|
||||
@@ -681,7 +721,6 @@ class SendOverride : Feature("Send Override") {
|
||||
}
|
||||
}
|
||||
|
||||
// Time Picker Dialog
|
||||
if (showTimePickerDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showTimePickerDialog = false },
|
||||
@@ -719,7 +758,6 @@ class SendOverride : Feature("Send Override") {
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
// Date Selection Button
|
||||
OutlinedButton(
|
||||
onClick = { showDatePickerDialog = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@@ -727,22 +765,19 @@ class SendOverride : Feature("Send Override") {
|
||||
Icon(Icons.Default.CalendarToday, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
datePickerState.selectedDateMillis?.let { millis ->
|
||||
SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(millis)
|
||||
datePickerState.selectedDateMillis?.let {
|
||||
SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(it)
|
||||
} ?: context.translation.getOrNull("select_date") ?: "Select Date"
|
||||
)
|
||||
}
|
||||
|
||||
// Time Selection Button
|
||||
OutlinedButton(
|
||||
onClick = { showTimePickerDialog = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(Icons.Default.Schedule, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
String.format("%02d:%02d", clockPickerHour, clockPickerMinute)
|
||||
)
|
||||
Text(String.format("%02d:%02d", clockPickerHour, clockPickerMinute))
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -799,6 +834,7 @@ class SendOverride : Feature("Send Override") {
|
||||
}
|
||||
Button(onClick = {
|
||||
alertDialog.dismiss()
|
||||
val finalSelectedType = selectedType
|
||||
val delayMs = scheduledTime?.let { it - System.currentTimeMillis() }
|
||||
if (delayMs != null && delayMs > 0) {
|
||||
val taskHash = java.util.UUID.randomUUID().toString()
|
||||
@@ -810,49 +846,42 @@ class SendOverride : Feature("Send Override") {
|
||||
recipientNameForTask,
|
||||
taskHash
|
||||
)
|
||||
val days = (delayMs / (24 * 60 * 60 * 1000)).toInt()
|
||||
val hours = ((delayMs / (60 * 60 * 1000)) % 24).toInt()
|
||||
val minutes = ((delayMs / (60 * 1000)) % 60).toInt()
|
||||
val seconds = ((delayMs / 1000) % 60).toInt()
|
||||
|
||||
val scheduledTimeText = StringBuilder().apply {
|
||||
if (days > 0) append("${days}d ")
|
||||
if (hours > 0 || days > 0) append("${hours}h ")
|
||||
if (minutes > 0 || hours > 0 || days > 0) append("${minutes}m ")
|
||||
append("${seconds}s")
|
||||
}.toString()
|
||||
val scheduledTimeText = formatTimeText(delayMs)
|
||||
context.inAppOverlay.showStatusToast(
|
||||
icon = Icons.Filled.Schedule,
|
||||
text = context.translation.format("schedule_scheduled_for", "name" to recipientNameForTask, "time" to scheduledTimeText) ?: "Scheduled for $recipientNameForTask in $scheduledTimeText"
|
||||
)
|
||||
val releaseBackground = acquireScheduledSendBackground()
|
||||
|
||||
event.addCallbackResult("onSuccess") {
|
||||
context.bridgeClient.getTaskInterface().successTask(taskHash)
|
||||
}
|
||||
event.addCallbackResult("onError") {
|
||||
context.bridgeClient.getTaskInterface().failTask(taskHash, it.getOrNull(0)?.toString() ?: "Unknown error")
|
||||
}
|
||||
|
||||
val job = context.coroutineScope.launch {
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (true) {
|
||||
delay(1000)
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
val remaining = delayMs - elapsed
|
||||
if (remaining <= 0) break
|
||||
val days = (remaining / (24 * 60 * 60 * 1000)).toInt()
|
||||
val hours = ((remaining / (60 * 60 * 1000)) % 24).toInt()
|
||||
val minutes = ((remaining / (60 * 1000)) % 60).toInt()
|
||||
val seconds = ((remaining / 1000) % 60).toInt()
|
||||
|
||||
val timeText = StringBuilder().apply {
|
||||
if (days > 0) append("${days}d ")
|
||||
if (hours > 0 || days > 0) append("${hours}h ")
|
||||
if (minutes > 0 || hours > 0 || days > 0) append("${minutes}m ")
|
||||
append("${seconds}s")
|
||||
}.toString()
|
||||
val timeText = formatTimeText(remaining)
|
||||
val progress = (elapsed * 100 / delayMs).toInt().coerceIn(0, 99)
|
||||
|
||||
context.bridgeClient.getTaskInterface().updateTaskProgress(
|
||||
taskHash,
|
||||
context.translation.getOrNull("schedule_sending_in")?.replace("{time}", timeText) ?: "Sending in $timeText",
|
||||
-1
|
||||
progress
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
if (sendMedia(selectedType, if (selectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) {
|
||||
|
||||
context.bridgeClient.getTaskInterface().updateTaskProgress(taskHash, "Sending...", 100)
|
||||
|
||||
if (sendMedia(finalSelectedType, if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) {
|
||||
event.invokeOriginal()
|
||||
context.bridgeClient.getTaskInterface().successTask(taskHash)
|
||||
val successText = context.translation.format("schedule_sent_to", "name" to recipientNameForTask) ?: "Sent to $recipientNameForTask"
|
||||
context.inAppOverlay.showStatusToast(
|
||||
icon = Icons.Filled.CheckCircle,
|
||||
@@ -899,7 +928,7 @@ class SendOverride : Feature("Send Override") {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (sendMedia(selectedType, if (selectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) {
|
||||
if (sendMedia(finalSelectedType, if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) {
|
||||
event.invokeOriginal()
|
||||
}
|
||||
}
|
||||
@@ -907,8 +936,6 @@ class SendOverride : Feature("Send Override") {
|
||||
Text(if (scheduledTime != null) mainTranslation["schedule"] else context.translation["button.send"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
@@ -15,11 +15,24 @@ class UnsaveableMessages : MessagingRuleFeature(
|
||||
) {
|
||||
private var shouldModifySavePolicy = false
|
||||
|
||||
private fun getEnabledFields(): List<Int> {
|
||||
val config = context.config.messaging.unsaveableMessages
|
||||
return buildList {
|
||||
if (config.chat.get()) add(2)
|
||||
if (config.externalMedia.get()) add(3)
|
||||
if (config.sticker.get()) add(4)
|
||||
if (config.share.get()) add(5)
|
||||
if (config.note.get()) add(6)
|
||||
if (config.storyReply.get()) add(7)
|
||||
if (config.snap.get()) add(11)
|
||||
}
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
val ruleState = context.config.rules.getRuleState(MessagingRuleType.UNSAVEABLE_MESSAGES)
|
||||
if (ruleState == null) return
|
||||
if (context.config.rules.getRuleState(MessagingRuleType.UNSAVEABLE_MESSAGES) == null) return
|
||||
|
||||
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
|
||||
if (event.canceled) return@subscribe
|
||||
shouldModifySavePolicy = false
|
||||
|
||||
if (event.destinations.stories?.isNotEmpty() == true && event.destinations.conversations?.isEmpty() == true) return@subscribe
|
||||
@@ -28,114 +41,70 @@ class UnsaveableMessages : MessagingRuleFeature(
|
||||
if (localMessageContent.contentType == ContentType.STATUS) return@subscribe
|
||||
|
||||
val conversationIds = event.destinations.conversations?.map { it.toString() } ?: emptyList()
|
||||
val shouldApply = if (conversationIds.isEmpty()) {
|
||||
true
|
||||
} else {
|
||||
conversationIds.all { canUseRule(it) }
|
||||
}
|
||||
val shouldApply = conversationIds.isEmpty() || conversationIds.all { canUseRule(it) }
|
||||
|
||||
if (!shouldApply) return@subscribe
|
||||
|
||||
val messageContentBytes = localMessageContent.content ?: return@subscribe
|
||||
|
||||
val config = context.config.messaging.unsaveableMessages
|
||||
val enabledFields = mutableListOf<Int>()
|
||||
if (config.chat.get()) enabledFields.add(2)
|
||||
if (config.snap.get()) enabledFields.add(11)
|
||||
if (config.externalMedia.get()) enabledFields.add(3)
|
||||
if (config.sticker.get()) enabledFields.add(4)
|
||||
if (config.share.get()) enabledFields.add(5)
|
||||
if (config.note.get()) enabledFields.add(6)
|
||||
if (config.storyReply.get()) enabledFields.add(7)
|
||||
|
||||
val enabledFields = getEnabledFields()
|
||||
val protoReader = ProtoReader(messageContentBytes)
|
||||
|
||||
val fieldPath = enabledFields.firstOrNull { fieldId ->
|
||||
protoReader.followPath(fieldId) != null
|
||||
} ?: return@subscribe
|
||||
val fieldPath = enabledFields.firstOrNull { protoReader.followPath(it) != null } ?: return@subscribe
|
||||
|
||||
shouldModifySavePolicy = true
|
||||
|
||||
// Set mSavePolicy on Java object
|
||||
try {
|
||||
val savePolicyEnumClass = runCatching {
|
||||
XposedHelpers.findClass("com.snapchat.client.messaging.SavePolicy",
|
||||
localMessageContent.instanceNonNull().javaClass.classLoader)
|
||||
}.getOrNull()
|
||||
val savePolicyEnumClass = XposedHelpers.findClass(
|
||||
"com.snapchat.client.messaging.SavePolicy",
|
||||
localMessageContent.instanceNonNull().javaClass.classLoader
|
||||
)
|
||||
|
||||
if (savePolicyEnumClass != null && savePolicyEnumClass.isEnum) {
|
||||
if (savePolicyEnumClass.isEnum) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val enumClass = savePolicyEnumClass as Class<out Enum<*>>
|
||||
val prohibitedEnum = runCatching {
|
||||
java.lang.Enum.valueOf(enumClass, "PROHIBITED")
|
||||
}.getOrNull()
|
||||
|
||||
if (prohibitedEnum != null) {
|
||||
val savePolicyField = localMessageContent.instanceNonNull().javaClass.declaredFields
|
||||
.find { it.name == "mSavePolicy" }
|
||||
|
||||
if (savePolicyField != null) {
|
||||
savePolicyField.isAccessible = true
|
||||
XposedHelpers.setObjectField(localMessageContent.instanceNonNull(), "mSavePolicy", prohibitedEnum)
|
||||
}
|
||||
}
|
||||
val prohibitedEnum = java.lang.Enum.valueOf(
|
||||
savePolicyEnumClass as Class<out Enum<*>>,
|
||||
"PROHIBITED"
|
||||
)
|
||||
XposedHelpers.setObjectField(localMessageContent.instanceNonNull(), "mSavePolicy", prohibitedEnum)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context.log.warn("UnsaveableMessages: Failed to set mSavePolicy: ${e.message}")
|
||||
}
|
||||
|
||||
// Modify proto bytes
|
||||
try {
|
||||
val modifiedContent = ProtoEditor(messageContentBytes).apply {
|
||||
localMessageContent.content = ProtoEditor(messageContentBytes).apply {
|
||||
edit(fieldPath) {
|
||||
if (getOrNull(7) != null) {
|
||||
remove(7)
|
||||
}
|
||||
remove(7)
|
||||
addVarInt(7, 1)
|
||||
}
|
||||
}.toByteArray()
|
||||
|
||||
localMessageContent.content = modifiedContent
|
||||
|
||||
} catch (e: Exception) {
|
||||
context.log.error("UnsaveableMessages: Failed to modify savePolicy in proto: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Modify in NativeUnaryCallEvent as fallback
|
||||
// Fallback: Modify in NativeUnaryCallEvent
|
||||
context.event.subscribe(NativeUnaryCallEvent::class) { event ->
|
||||
if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe
|
||||
if (!shouldModifySavePolicy) return@subscribe
|
||||
|
||||
try {
|
||||
val config = context.config.messaging.unsaveableMessages
|
||||
val enabledFields = mutableListOf<Int>()
|
||||
if (config.chat.get()) enabledFields.add(2)
|
||||
if (config.snap.get()) enabledFields.add(11)
|
||||
if (config.externalMedia.get()) enabledFields.add(3)
|
||||
if (config.sticker.get()) enabledFields.add(4)
|
||||
if (config.share.get()) enabledFields.add(5)
|
||||
if (config.note.get()) enabledFields.add(6)
|
||||
if (config.storyReply.get()) enabledFields.add(7)
|
||||
|
||||
val enabledFields = getEnabledFields()
|
||||
val protoReader = ProtoReader(event.buffer)
|
||||
val fieldPath = enabledFields.firstOrNull { fieldId ->
|
||||
protoReader.followPath(fieldId) != null
|
||||
} ?: return@subscribe
|
||||
val fieldPath = enabledFields.firstOrNull { protoReader.followPath(it) != null } ?: return@subscribe
|
||||
|
||||
try {
|
||||
event.buffer = ProtoEditor(event.buffer).apply {
|
||||
edit(fieldPath) {
|
||||
if (getOrNull(7) != null) {
|
||||
remove(7)
|
||||
}
|
||||
addVarInt(7, 1)
|
||||
}
|
||||
}.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
context.log.error("UnsaveableMessages: NativeUnaryCallEvent proto modification failed for field $fieldPath: ${e.message}")
|
||||
}
|
||||
shouldModifySavePolicy = false
|
||||
event.buffer = ProtoEditor(event.buffer).apply {
|
||||
edit(fieldPath) {
|
||||
remove(7)
|
||||
addVarInt(7, 1)
|
||||
}
|
||||
}.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
context.log.error("UnsaveableMessages: NativeUnaryCallEvent modification failed: ${e.message}")
|
||||
} finally {
|
||||
shouldModifySavePolicy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user