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.
22 changes: 22 additions & 0 deletions app/src/main/java/xyz/rodit/snapmod/DelegateProxy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package xyz.rodit.snapmod

import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

typealias DelegateFunction = (Any, Array<Any>) -> Any?

class DelegateProxy(private val delegate: DelegateFunction) : InvocationHandler {

override fun invoke(target: Any, method: Method, args: Array<Any>?): Any? {
return delegate(target, args ?: emptyArray())
}
}

fun Class<*>.createDelegate(classLoader: ClassLoader, delegate: DelegateFunction): Any {
return Proxy.newProxyInstance(
classLoader,
arrayOf(this),
DelegateProxy(delegate)
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package xyz.rodit.snapmod.features

import xyz.rodit.snapmod.features.chatmenu.ChatMenuModifier
import xyz.rodit.snapmod.features.chatmenu.new.NewChatMenuModifier
import xyz.rodit.snapmod.features.conversations.*
import xyz.rodit.snapmod.features.friendsfeed.FeedModifier
import xyz.rodit.snapmod.features.info.AdditionalFriendInfo
Expand All @@ -22,6 +23,7 @@ class FeatureManager(context: FeatureContext) : Contextual(context) {
fun load() {
// Chat context menu
add(::ChatMenuModifier)
add(::NewChatMenuModifier)

// Friends feed
add(::FeedModifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package xyz.rodit.snapmod.features.callbacks
import de.robv.android.xposed.XC_MethodHook
import xyz.rodit.dexsearch.client.xposed.MethodRef
import xyz.rodit.snapmod.isDummyProxy
import xyz.rodit.snapmod.mappings.DefaultFetchConversationCallback
import xyz.rodit.snapmod.mappings.DefaultFetchMessageCallback
import xyz.rodit.snapmod.util.before
import kotlin.reflect.KClass

Expand All @@ -28,4 +30,16 @@ class CallbackManager {
fun on(type: KClass<*>, method: MethodRef, callback: HookedCallback) {
callbacks.computeIfAbsent("${type.simpleName}:${method.name}") { mutableListOf() }.add(callback)
}

init {
hook(
DefaultFetchConversationCallback::class,
DefaultFetchConversationCallback.onFetchConversationWithMessagesComplete
) { DefaultFetchConversationCallback.wrap(it).dummy }

hook(
DefaultFetchMessageCallback::class,
DefaultFetchMessageCallback.onFetchMessageComplete
) { DefaultFetchMessageCallback.wrap(it).dummy }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const val PIN_STRING_NAME = "action_menu_pin_conversation"

class ChatMenuModifier(context: FeatureContext) : Feature(context) {

private val plugins: MutableMap<String, MenuPlugin> = HashMap()
private val plugins = mutableMapOf<String, MenuPlugin>()

override fun init() {
registerPlugin(PreviewOption(context))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
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.*
import xyz.rodit.snapmod.features.chatmenu.shared.export

class ExportOption(context: FeatureContext):
ButtonOption(context, "export_chat", menu_option_export) {
Expand All @@ -17,41 +12,6 @@ class ExportOption(context: FeatureContext):
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"))
export(context, data)
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
package xyz.rodit.snapmod.features.chatmenu

import android.app.AlertDialog
import de.robv.android.xposed.XC_MethodHook
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.*
import xyz.rodit.snapmod.util.toSnapUUID
import xyz.rodit.snapmod.util.toUUIDString
import java.lang.Integer.min
import xyz.rodit.snapmod.features.chatmenu.shared.previewChat

class PreviewOption(context: FeatureContext) :
ButtonOption(context, "preview", menu_option_preview) {
Expand All @@ -18,79 +12,6 @@ class PreviewOption(context: FeatureContext) :
override fun handleEvent(data: String?) {
if (data == null) return

val uuid = data.toSnapUUID()
val proxy =
ConversationDummyInterface.wrap(
ConversationDummyInterface.getMappedClass().createDummyProxy(context.classLoader)
)

context.callbacks.on(
DefaultFetchConversationCallback::class,
DefaultFetchConversationCallback.onFetchConversationWithMessagesComplete,
this::displayPreview
)

context.instances.conversationManager.fetchConversationWithMessages(
uuid,
DefaultFetchConversationCallback(proxy, uuid, false)
)
}

override fun performHooks() {
context.callbacks.hook(
DefaultFetchConversationCallback::class,
DefaultFetchConversationCallback.onFetchConversationWithMessagesComplete
) { DefaultFetchConversationCallback.wrap(it).dummy }
}

private fun displayPreview(param: XC_MethodHook.MethodHookParam): Boolean {
val conversation = Conversation.wrap(param.args[0])

val userIds = conversation.participants.map(Participant::wrap)
.map { p -> (p.participantId.id as ByteArray).toUUIDString() }
val friendData = context.instances.friendsRepository.selectFriendsByUserIds(userIds)
val userMap = friendData.map(SelectFriendsByUserIds::wrap).associateBy { u -> u.userId }

val messageList = param.args[1] as List<*>
val previewText = StringBuilder()
if (messageList.isEmpty()) previewText.append("No messages available.")
else {
val numMessages =
min(context.config.getInt("preview_messages_count", 5), messageList.size)
previewText.append("Last ").append(numMessages).append(" messages:")
messageList.takeLast(numMessages)
.map(Message::wrap).forEach { m ->
run {
val uuidString = m.senderId.toUUIDString()
val displayName = userMap[uuidString]?.displayName ?: "Unknown"
previewText.append('\n').append(displayName).append(": ")
if (m.messageContent.contentType.instance == ContentType.CHAT().instance) {
val chatMessage =
NanoMessageContent.parse(m.messageContent.content).chatMessageContent.content
previewText.append(chatMessage)
} else {
previewText.append(m.messageContent.contentType.instance)
}
}
}
}

userMap.values.find { f -> f.streakExpiration ?: 0L > 0L }?.let { f ->
val hourDiff =
(f.streakExpiration - System.currentTimeMillis()).toDouble() / 3600000.0
previewText.append("\n\nStreak Expires in ")
.append(String.format("%.1f", hourDiff))
.append(" hours")
}

context.activity?.runOnUiThread {
AlertDialog.Builder(context.activity)
.setTitle(if (conversation.title.isNullOrBlank()) "Chat Preview" else conversation.title)
.setMessage(previewText)
.setPositiveButton("Ok") { _, _ -> }
.show()
}

return true
previewChat(context, data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package xyz.rodit.snapmod.features.chatmenu.new

abstract class MenuPlugin {

abstract fun shouldCreate(): Boolean

abstract fun createModel(key: String): Any

open fun performHooks() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package xyz.rodit.snapmod.features.chatmenu.new

import xyz.rodit.snapmod.createDelegate
import xyz.rodit.snapmod.features.Feature
import xyz.rodit.snapmod.features.FeatureContext
import xyz.rodit.snapmod.features.chatmenu.shared.export
import xyz.rodit.snapmod.features.chatmenu.shared.previewChat
import xyz.rodit.snapmod.mappings.*
import xyz.rodit.snapmod.util.before

class NewChatMenuModifier(context: FeatureContext) : Feature(context) {

private val plugins = mutableListOf<MenuPlugin>()

override fun init() {
registerPlain("Export") { export(context, it) }
registerPlain("Preview") { previewChat(context, it) }

registerSwitch("pinning", "Pin Conversation") { it.pinned }
registerSwitch("stealth", "Stealth Mode") { it.stealth }
registerSwitch("auto_save", "Auto-Save Messages") { it.autoSave }
registerSwitch("auto_download", "Auto-Download Snaps") { it.autoDownload }
}

private fun registerPlugin(plugin: MenuPlugin) {
plugins.add(plugin)
}

private fun registerPlain(text: String, click: ClickHandler) {
registerPlugin(PlainOption(context, text, click))
}

private fun registerSwitch(name: String, text: String, manager: Manager) {
registerPlugin(SwitchOption(context, name, text, manager))
}

override fun performHooks() {
// Force new chat action menu
ProfileActionSheetChooser.choose.before {
it.args[0] = context.config.getBoolean("enable_new_chat_menu", true)
}

// Add subsection
ProfileActionSheetCreator.apply.before {
if (it.args[0] !is List<*>) return@before

val newItems = (it.args[0] as List<*>).toMutableList()
val creator = ProfileActionSheetCreator.wrap(it.thisObject)
val nestedContext = NestedActionMenuContext.wrap(creator.nestedContext)
val actionContext = ActionMenuContext.wrap(creator.actionMenuContext)
val key = actionContext.feedInfo.key

val subOptions = plugins.filter(MenuPlugin::shouldCreate).map { p ->
p.createModel(key)
}
val clickProxy =
Func0.getMappedClass().createDelegate(context.classLoader) { _, _ ->
NestedActionMenuContext.display(
nestedContext,
"SnapMod",
subOptions
)
null
}
val snapModSettings =
ActionClickableCaret("SnapMod Settings", null, Func0.wrap(clickProxy)).instance
newItems.add(snapModSettings)

it.args[0] = newItems
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package xyz.rodit.snapmod.features.chatmenu.new

import xyz.rodit.snapmod.createDelegate
import xyz.rodit.snapmod.features.FeatureContext
import xyz.rodit.snapmod.mappings.ActionPlain
import xyz.rodit.snapmod.mappings.Func0

typealias ClickHandler = (key: String) -> Unit

class PlainOption(
private val context: FeatureContext,
private val text: String,
private val click: ClickHandler
) : MenuPlugin() {

override fun shouldCreate() = true

override fun createModel(key: String): Any = ActionPlain(
text,
Func0.wrap(Func0.getMappedClass().createDelegate(context.classLoader) { _, _ ->
click(key)
})
).instance
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package xyz.rodit.snapmod.features.chatmenu.new

import xyz.rodit.snapmod.createDelegate
import xyz.rodit.snapmod.features.FeatureContext
import xyz.rodit.snapmod.mappings.ActionSwitch
import xyz.rodit.snapmod.mappings.Func1
import xyz.rodit.snapmod.util.ConversationManager
import xyz.rodit.snapmod.util.getList

typealias Manager = (FeatureContext) -> ConversationManager

class SwitchOption(
private val context: FeatureContext,
private val name: String,
private val text: String,
private val manager: Manager
) : MenuPlugin() {

override fun shouldCreate() = !context.config.getList("hidden_chat_options").contains(name)

override fun createModel(key: String): Any = ActionSwitch(
text,
manager(context).isEnabled(key),
Func1.wrap(Func1.getMappedClass().createDelegate(context.classLoader) { _, _ -> true }),
Func1.wrap(Func1.getMappedClass().createDelegate(context.classLoader) { _, _ ->
manager(context).toggle(key)
true
}),
null,
0
).instance
}
Loading