diff --git a/app/libs/snapmod.jar b/app/libs/snapmod.jar index b4eb64c..3bdce52 100644 Binary files a/app/libs/snapmod.jar and b/app/libs/snapmod.jar differ diff --git a/app/src/main/java/xyz/rodit/snapmod/CustomResources.kt b/app/src/main/java/xyz/rodit/snapmod/CustomResources.kt index fcafb89..03a2efd 100644 --- a/app/src/main/java/xyz/rodit/snapmod/CustomResources.kt +++ b/app/src/main/java/xyz/rodit/snapmod/CustomResources.kt @@ -12,6 +12,7 @@ object CustomResources { string.menu_option_preview to "More Information", string.menu_option_auto_save to "Auto-Save Messages", string.menu_option_auto_download to "Auto-Download Snaps", + string.menu_option_export to "Export...", string.chat_action_playback_speed to "Set Playback Speed" ) @@ -41,6 +42,7 @@ object CustomResources { const val menu_option_preview = -100001 const val menu_option_auto_save = -100002 const val menu_option_auto_download = -100003 + const val menu_option_export = -100004 const val chat_action_playback_speed = -200000 } diff --git a/app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoMessage.kt b/app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoMessage.kt new file mode 100644 index 0000000..5f7bf22 --- /dev/null +++ b/app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoMessage.kt @@ -0,0 +1,3 @@ +package xyz.rodit.snapmod.arroyo + +data class ArroyoMessage(val content: String, val timestamp: Long, val senderId: String) \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoReader.kt b/app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoReader.kt index 1c6c8ba..d7d3e5f 100644 --- a/app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoReader.kt +++ b/app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoReader.kt @@ -11,7 +11,33 @@ class ArroyoReader(private val context: Context) { fun getMessageContent(conversationId: String, messageId: String): String? { val blob = getMessageBlob(conversationId, messageId) ?: return null - return followProtoString(blob, 4, 4, 2, 1) + return readChatMessageContent(blob) + } + + fun getAllMessages(conversationId: String, after: Long = 0): Pair, Set> { + val messages = mutableListOf() + val senderIds = hashSetOf() + SQLiteDatabase.openDatabase( + File(context.filesDir, "../databases/arroyo.db").path, + null, + 0 + ).use { + it.rawQuery( + "SELECT message_content,creation_timestamp,sender_id FROM conversation_message WHERE client_conversation_id='$conversationId' AND creation_timestamp>$after AND content_type=1 ORDER BY creation_timestamp ASC", + null + ).use { cursor -> + while (cursor.moveToNext()) { + val content = cursor.getBlob(0) + val timestamp = cursor.getLong(1) + val senderId = cursor.getString(2) + val contentString = readChatMessageContent(content) ?: continue + messages.add(ArroyoMessage(contentString, timestamp, senderId)) + senderIds.add(senderId) + } + } + } + + return messages to senderIds } fun getKeyAndIv(conversationId: String, messageId: String): Pair? { @@ -28,6 +54,10 @@ class ArroyoReader(private val context: Context) { return key to iv } + private fun readChatMessageContent(blob: ByteArray): String? { + return followProtoString(blob, 4, 4, 2, 1) + } + private fun followProtoString(data: ByteArray, vararg indices: Int): String? { val proto = followProto(data, *indices) return if (proto != null) String(proto) else null diff --git a/app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt b/app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt index 0574331..be4d182 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt @@ -2,6 +2,7 @@ package xyz.rodit.snapmod.features import android.app.Activity import android.content.Context +import xyz.rodit.snapmod.arroyo.ArroyoReader import xyz.rodit.snapmod.features.callbacks.CallbackManager import xyz.rodit.snapmod.util.ConversationManager import xyz.rodit.xposed.client.ConfigurationClient @@ -27,6 +28,7 @@ class FeatureContext( val stealth: ConversationManager = ConversationManager(appContext.filesDir, STEALTH_CONVERSATIONS_FILE) val autoSave: ConversationManager = ConversationManager(appContext.filesDir, AUTO_SAVE_CONVERSATIONS_FILE) val autoDownload: ConversationManager = ConversationManager(appContext.filesDir, AUTO_DOWNLOAD_CONVERSATIONS_FILE) + val arroyo = ArroyoReader(appContext) var activity: Activity? = null } \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt index 5053556..b984e53 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt @@ -23,6 +23,7 @@ class ChatMenuModifier(context: FeatureContext) : Feature(context) { override fun init() { registerPlugin(PreviewOption(context)) + registerPlugin(ExportOption(context)) val pinTextResource = context.appContext.resources.getIdentifier( PIN_STRING_NAME, diff --git a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ExportOption.kt b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ExportOption.kt new file mode 100644 index 0000000..3be9a64 --- /dev/null +++ b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ExportOption.kt @@ -0,0 +1,57 @@ +package xyz.rodit.snapmod.features.chatmenu + +import android.content.Intent +import androidx.core.content.FileProvider +import xyz.rodit.snapmod.CustomResources.string.menu_option_export +import xyz.rodit.snapmod.features.FeatureContext +import xyz.rodit.snapmod.mappings.SelectFriendsByUserIds +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + +class ExportOption(context: FeatureContext): + ButtonOption(context, "export_chat", menu_option_export) { + + override fun shouldCreate() = true + + override fun handleEvent(data: String?) { + if (data == null) return + + val (messages, senders) = context.arroyo.getAllMessages(data) + val friendData = + context.instances.friendsRepository.selectFriendsByUserIds(senders.toList()) + val senderMap = friendData.map(SelectFriendsByUserIds::wrap).associateBy { u -> u.userId } + + val dateFormat = SimpleDateFormat("dd/MM/yyyy, HH:mm:ss", Locale.getDefault()) + + val temp = File.createTempFile( + "Snapchat Export ", + ".txt", + File(context.appContext.filesDir, "file_manager/media") + ) + temp.deleteOnExit() + temp.bufferedWriter().use { + messages.forEach { m -> + val username = senderMap[m.senderId]?.displayName ?: "Unknown" + val dateTime = dateFormat.format(m.timestamp) + it.append(dateTime) + .append(" - ") + .append(username) + .append(": ") + .appendLine(m.content) + } + } + + val intent = Intent(Intent.ACTION_SEND) + .setType("text/plain") + .putExtra( + Intent.EXTRA_STREAM, + FileProvider.getUriForFile( + context.appContext, + "com.snapchat.android.media.fileprovider", + temp + ) + ) + context.activity?.startActivity(Intent.createChooser(intent, "Export Chat")) + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/PreviewOption.kt b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/PreviewOption.kt index 8bda6cb..b758fda 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/PreviewOption.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/PreviewOption.kt @@ -2,7 +2,7 @@ package xyz.rodit.snapmod.features.chatmenu import android.app.AlertDialog import de.robv.android.xposed.XC_MethodHook -import xyz.rodit.snapmod.CustomResources +import xyz.rodit.snapmod.CustomResources.string.menu_option_preview import xyz.rodit.snapmod.createDummyProxy import xyz.rodit.snapmod.features.FeatureContext import xyz.rodit.snapmod.mappings.* @@ -11,11 +11,9 @@ import xyz.rodit.snapmod.util.toUUIDString import java.lang.Integer.min class PreviewOption(context: FeatureContext) : - ButtonOption(context, "preview", CustomResources.string.menu_option_preview) { + ButtonOption(context, "preview", menu_option_preview) { - override fun shouldCreate(): Boolean { - return true - } + override fun shouldCreate() = true override fun handleEvent(data: String?) { if (data == null) return diff --git a/app/src/main/java/xyz/rodit/snapmod/features/conversations/MessageInterceptor.kt b/app/src/main/java/xyz/rodit/snapmod/features/conversations/MessageInterceptor.kt index 32438e8..756882c 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/conversations/MessageInterceptor.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/conversations/MessageInterceptor.kt @@ -12,7 +12,10 @@ class MessageInterceptor(context: FeatureContext) : StealthFeature(context) { putFilters( ConversationManager.sendMessageWithContent, { LocalMessageContent.wrap(it.args[1]).contentType }, - { MessageDestinations.wrap(it.args[0]).conversations[0].toUUIDString() }, + { + val conversations = MessageDestinations.wrap(it.args[0]).conversations + if (conversations.isNotEmpty()) conversations[0].toUUIDString() else null + }, ObjectFilter( context, "hide_screenshot", @@ -27,15 +30,12 @@ class MessageInterceptor(context: FeatureContext) : StealthFeature(context) { { MessageUpdate.wrap(it.args[2]) }, { it.args[0].toUUIDString() }, ObjectFilter(context, "hide_read", MessageUpdate.READ()), - ObjectFilter(context, "hide_save", MessageUpdate.SAVE(), MessageUpdate.UNSAVE()), ObjectFilter( context, "hide_screenshot", MessageUpdate.SCREENSHOT(), MessageUpdate.SCREEN_RECORD() - ), - ObjectFilter(context, "hide_replay", MessageUpdate.REPLAY()), - ObjectFilter(context, "dont_release", MessageUpdate.RELEASE()) + ) ) } } \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/conversations/StealthFeature.kt b/app/src/main/java/xyz/rodit/snapmod/features/conversations/StealthFeature.kt index 06f1b14..8f09d04 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/conversations/StealthFeature.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/conversations/StealthFeature.kt @@ -10,7 +10,7 @@ import xyz.rodit.snapmod.features.FeatureContext import xyz.rodit.snapmod.features.shared.Filter typealias FilterObjectSupplier = (MethodHookParam) -> Any? -typealias ConversationIdSupplier = (MethodHookParam) -> String +typealias ConversationIdSupplier = (MethodHookParam) -> String? abstract class StealthFeature(context: FeatureContext) : Feature(context) { @@ -55,7 +55,7 @@ abstract class StealthFeature(context: FeatureContext) : Feature(context) { MappedObject.hook(className, methodName, object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam) { val obj = supplier?.invoke(param) - val id = conversationIdSupplier!!.invoke(param) + val id = conversationIdSupplier?.invoke(param) ?: return val stealth = context.stealth.isEnabled(id) if (filters[methodName]!!.any { f -> diff --git a/app/src/main/java/xyz/rodit/snapmod/features/notifications/ShowMessageContent.kt b/app/src/main/java/xyz/rodit/snapmod/features/notifications/ShowMessageContent.kt index 187240d..8dcd54d 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/notifications/ShowMessageContent.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/notifications/ShowMessageContent.kt @@ -17,7 +17,6 @@ import androidx.core.app.NotificationCompat import com.google.gson.Gson import com.google.gson.JsonArray import xyz.rodit.snapmod.Shared -import xyz.rodit.snapmod.arroyo.ArroyoReader import xyz.rodit.snapmod.features.Feature import xyz.rodit.snapmod.features.FeatureContext import xyz.rodit.snapmod.logging.log @@ -38,7 +37,6 @@ private val imageSig = listOf( class ShowMessageContent(context: FeatureContext) : Feature(context, 84608.toMax()) { - private val arroyoReader = ArroyoReader(context.appContext) private val gson: Gson = Gson() private val notifications @@ -78,9 +76,9 @@ class ShowMessageContent(context: FeatureContext) : Feature(context, 84608.toMax val snap = type == "snap" val (key, iv) = (if (snap) - arroyoReader.getSnapKeyAndIv(conversationId, messageId) + context.arroyo.getSnapKeyAndIv(conversationId, messageId) else - arroyoReader.getKeyAndIv(conversationId, messageId)) ?: return@before + context.arroyo.getKeyAndIv(conversationId, messageId)) ?: return@before val media = getDownloadUrls(mediaInfo) val crypt = AesCrypto(key, iv) @@ -92,10 +90,10 @@ class ShowMessageContent(context: FeatureContext) : Feature(context, 84608.toMax val group = idProvider.conversationIdentifier.group val title = (bundle.getString("ab_cnotif_body") ?: "sent Media") + - if (snap) - " (${if (isImage) "Image" else "Video"})" - else - " (Media x ${media.size})" + if (snap) + " (${if (isImage) "Image" else "Video"})" + else + " (Media x ${media.size})" val notification = NotificationCompat.Builder(context.appContext, CHANNEL_ID) .setContentTitle(bundle.getString("sender") ?: "Unknown Sender") @@ -118,7 +116,9 @@ class ShowMessageContent(context: FeatureContext) : Feature(context, 84608.toMax return@before } - val content = arroyoReader.getMessageContent(conversationId, messageId) ?: return@before + val content = + context.arroyo.getMessageContent(conversationId, messageId) ?: return@before + bundle.putString("subtitle", content) bundle.putString("ab_cnotif_body", content) } } diff --git a/app/src/main/java/xyz/rodit/snapmod/features/saving/ChatSaving.kt b/app/src/main/java/xyz/rodit/snapmod/features/saving/ChatSaving.kt index 20fca01..dbe7730 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/saving/ChatSaving.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/saving/ChatSaving.kt @@ -65,40 +65,43 @@ class ChatSaving(context: FeatureContext) : Feature(context) { val base = ChatModelBase.wrap(it.args[2]) lastMessageData = base.messageData - if (ChatModelLiveSnap.isInstance(it.args[2])) { - // Convert live snap to saved snap. - val hashCode = it.args[2].hashCode() - val media = LiveSnapMedia.wrap(chatMediaMap[hashCode]) - it.args[2] = ChatModelSavedSnap( - base.context, - base.messageData, - base.senderId, - emptyMap(), - true, - base.reactionsViewModel, - true, - 0, - 0, - media, - null, - base.status, - true, - true - ).instance - } else if (ChatModelAudioNote.isInstance(it.args[2])) { - val audio = ChatModelAudioNote.wrap(it.args[2]) - resolveAndDownload(audio.uri, base.messageData) - - it.result = null - } else if (ChatModelPlugin.isInstance(it.args[2])) { - val messageData = base.messageData - - if (messageData.type != "audio_note") return@before - val media = GallerySnapMedia.wrap(messageData.media.instance).media - val uri = createMediaUri(messageData.arroyoMessageId, media.id) - - resolveAndDownload(uri, messageData) - it.result = null + when { + ChatModelLiveSnap.isInstance(it.args[2]) -> { + // Convert live snap to saved snap. + val hashCode = it.args[2].hashCode() + val media = LiveSnapMedia.wrap(chatMediaMap[hashCode]) + it.args[2] = ChatModelSavedSnap( + base.context, + base.messageData, + base.senderId, + emptyMap(), + true, + base.reactionsViewModel, + true, + 0, + 0, + media, + null, + base.status, + true, + true + ).instance + } + ChatModelAudioNote.isInstance(it.args[2]) -> { + val audio = ChatModelAudioNote.wrap(it.args[2]) + resolveAndDownload(audio.uri, base.messageData) + it.result = null + } + ChatModelPlugin.isInstance(it.args[2]) -> { + val messageData = base.messageData + + if (messageData.type != "audio_note") return@before + val media = GallerySnapMedia.wrap(messageData.media.instance).media + val uri = createMediaUri(messageData.arroyoMessageId, media.id) + + resolveAndDownload(uri, messageData) + it.result = null + } } } diff --git a/app/src/main/java/xyz/rodit/snapmod/features/tweaks/BypassVideoLengthGlobal.kt b/app/src/main/java/xyz/rodit/snapmod/features/tweaks/BypassVideoLengthGlobal.kt index be897c2..27902d3 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/tweaks/BypassVideoLengthGlobal.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/tweaks/BypassVideoLengthGlobal.kt @@ -2,6 +2,7 @@ package xyz.rodit.snapmod.features.tweaks import xyz.rodit.snapmod.features.Feature import xyz.rodit.snapmod.features.FeatureContext +import xyz.rodit.snapmod.mappings.CameraRollVideoLengthChecker import xyz.rodit.snapmod.mappings.MediaPackage import xyz.rodit.snapmod.mappings.VideoLengthChecker import xyz.rodit.snapmod.util.after @@ -26,5 +27,12 @@ class BypassVideoLengthGlobal(context: FeatureContext) : Feature(context, 84606. MediaPackage.wrap(it.args[0]).media.videoDurationMs = lastVideoDuration } + + CameraRollVideoLengthChecker.isOver60Seconds.before( + context, + "bypass_video_length_restrictions" + ) { + it.result = false + } } } \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/util/PathManager.kt b/app/src/main/java/xyz/rodit/snapmod/util/PathManager.kt index fca31af..5d0b34d 100644 --- a/app/src/main/java/xyz/rodit/snapmod/util/PathManager.kt +++ b/app/src/main/java/xyz/rodit/snapmod/util/PathManager.kt @@ -15,7 +15,7 @@ object PathManager { const val DOWNLOAD_PROFILE = "profile" const val DOWNLOAD_SNAP = "snap" - private const val DEFAULT_DATE_FORMAT = "dd-MM-yyyy_HH:mm:ss" + private const val DEFAULT_DATE_FORMAT = "dd-MM-yyyy_HH-mm-ss" private val PATTERN_PUBLIC_DIR = Pattern.compile("""\$(\w+)""") private val PATTERN_PARAMETER = Pattern.compile("%([A-Za-z]+)") diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index c9e8c63..e3cf605 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -91,12 +91,6 @@ - -