Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified app/libs/snapmod.jar
Binary file not shown.
2 changes: 2 additions & 0 deletions app/src/main/java/xyz/rodit/snapmod/CustomResources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoMessage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package xyz.rodit.snapmod.arroyo

data class ArroyoMessage(val content: String, val timestamp: Long, val senderId: String)
32 changes: 31 additions & 1 deletion app/src/main/java/xyz/rodit/snapmod/arroyo/ArroyoReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<ArroyoMessage>, Set<String>> {
val messages = mutableListOf<ArroyoMessage>()
val senderIds = hashSetOf<String>()
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<ByteArray, ByteArray>? {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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())
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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)
}
}
Expand Down
71 changes: 37 additions & 34 deletions app/src/main/java/xyz/rodit/snapmod/features/saving/ChatSaving.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Any?, Any>(),
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<Any?, Any>(),
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
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/xyz/rodit/snapmod/util/PathManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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]+)")
Expand Down
6 changes: 0 additions & 6 deletions app/src/main/res/xml/root_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,6 @@
<PreferenceCategory app:title="@string/tweaks_header"
app:iconSpaceReserved="false">

<SwitchPreferenceCompat
app:key="save_any"
app:title="@string/save_any_title"
app:summary="@string/save_any_description"
app:iconSpaceReserved="false" />

<SwitchPreferenceCompat
app:key="allow_pin_chats"
app:title="@string/allow_pin_chats_title"
Expand Down
Loading