refactor: conversation manager wrapper

- fix auto save in background
This commit is contained in:
rhunk 2023-11-02 14:08:02 +01:00
parent 8d1c9a87ad
commit 17f81eb682
18 changed files with 251 additions and 325 deletions

View File

@ -105,6 +105,11 @@ enum class MediaReferenceType {
UNASSIGNED, OVERLAY, IMAGE, VIDEO, ASSET_BUNDLE, AUDIO, ANIMATED_IMAGE, FONT, WEB_VIEW_CONTENT, VIDEO_NO_AUDIO UNASSIGNED, OVERLAY, IMAGE, VIDEO, ASSET_BUNDLE, AUDIO, ANIMATED_IMAGE, FONT, WEB_VIEW_CONTENT, VIDEO_NO_AUDIO
} }
enum class MessageUpdate {
UNKNOWN, READ, RELEASE, SAVE, UNSAVE, ERASE, SCREENSHOT, SCREEN_RECORD, REPLAY, REACTION, REMOVEREACTION, REVOKETRANSCRIPTION, ALLOWTRANSCRIPTION, ERASESAVEDSTORYMEDIA
}
enum class FriendLinkType(val value: Int, val shortName: String) { enum class FriendLinkType(val value: Int, val shortName: String) {
MUTUAL(0, "mutual"), MUTUAL(0, "mutual"),
OUTGOING(1, "outgoing"), OUTGOING(1, "outgoing"),

View File

@ -1,7 +0,0 @@
package me.rhunk.snapenhance.common.util.ktx
fun String.longHashCode(): Long {
var h = 1125899906842597L
for (element in this) h = 31 * h + element.code.toLong()
return h
}

View File

@ -0,0 +1,36 @@
package me.rhunk.snapenhance.common.util.ktx
import java.lang.reflect.Field
fun String.longHashCode(): Long {
var h = 1125899906842597L
for (element in this) h = 31 * h + element.code.toLong()
return h
}
inline fun Class<*>.findFields(once: Boolean, crossinline predicate: (field: Field) -> Boolean): List<Field>{
var clazz: Class<*>? = this
val fields = mutableListOf<Field>()
while (clazz != null) {
if (once) {
clazz.declaredFields.firstOrNull(predicate)?.let { return listOf(it) }
} else {
fields.addAll(clazz.declaredFields.filter(predicate))
}
clazz = clazz.superclass ?: break
}
return fields
}
inline fun Class<*>.findFieldsToString(instance: Any? = null, once: Boolean = false, crossinline predicate: (field: Field, value: String) -> Boolean): List<Field> {
return this.findFields(once = once) {
try {
it.isAccessible = true
return@findFields it.get(instance)?.let { it1 -> predicate(it, it1.toString()) } == true
} catch (e: Throwable) {
return@findFields false
}
}
}

View File

@ -67,6 +67,8 @@ class ModContext(
val isDeveloper by lazy { config.scripting.developerMode.get() } val isDeveloper by lazy { config.scripting.developerMode.get() }
var isMainActivityPaused = false
fun <T : Feature> feature(featureClass: KClass<T>): T { fun <T : Feature> feature(featureClass: KClass<T>): T {
return features.get(featureClass)!! return features.get(featureClass)!!
} }

View File

@ -35,7 +35,6 @@ class SnapEnhance {
} }
private lateinit var appContext: ModContext private lateinit var appContext: ModContext
private var isBridgeInitialized = false private var isBridgeInitialized = false
private var isActivityPaused = false
private fun hookMainActivity(methodName: String, stage: HookStage = HookStage.AFTER, block: Activity.() -> Unit) { private fun hookMainActivity(methodName: String, stage: HookStage = HookStage.AFTER, block: Activity.() -> Unit) {
Activity::class.java.hook(methodName, stage, { isBridgeInitialized }) { param -> Activity::class.java.hook(methodName, stage, { isBridgeInitialized }) { param ->
@ -91,14 +90,14 @@ class SnapEnhance {
hookMainActivity("onPause") { hookMainActivity("onPause") {
appContext.bridgeClient.closeSettingsOverlay() appContext.bridgeClient.closeSettingsOverlay()
isActivityPaused = true appContext.isMainActivityPaused = true
} }
var activityWasResumed = false var activityWasResumed = false
//we need to reload the config when the app is resumed //we need to reload the config when the app is resumed
//FIXME: called twice at first launch //FIXME: called twice at first launch
hookMainActivity("onResume") { hookMainActivity("onResume") {
isActivityPaused = false appContext.isMainActivityPaused = false
if (!activityWasResumed) { if (!activityWasResumed) {
activityWasResumed = true activityWasResumed = true
return@hookMainActivity return@hookMainActivity
@ -175,7 +174,7 @@ class SnapEnhance {
} }
fun runLater(task: () -> Unit) { fun runLater(task: () -> Unit) {
if (isActivityPaused) { if (appContext.isMainActivityPaused) {
tasks.add(task) tasks.add(task)
} else { } else {
task() task()

View File

@ -18,18 +18,11 @@ import me.rhunk.snapenhance.core.logger.CoreLogger
import me.rhunk.snapenhance.core.messaging.ExportFormat import me.rhunk.snapenhance.core.messaging.ExportFormat
import me.rhunk.snapenhance.core.messaging.MessageExporter import me.rhunk.snapenhance.core.messaging.MessageExporter
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.util.CallbackBuilder
import me.rhunk.snapenhance.core.wrapper.impl.Message import me.rhunk.snapenhance.core.wrapper.impl.Message
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import java.io.File import java.io.File
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class ExportChatMessages : AbstractAction() { class ExportChatMessages : AbstractAction() {
private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") }
private val fetchConversationWithMessagesPaginatedMethod by lazy {
context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" }
}
private val dialogLogs = mutableListOf<String>() private val dialogLogs = mutableListOf<String>()
private var currentActionDialog: AlertDialog? = null private var currentActionDialog: AlertDialog? = null
@ -149,24 +142,14 @@ class ExportChatMessages : AbstractAction() {
} }
} }
private suspend fun fetchMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int) = suspendCancellableCoroutine { continuation -> private suspend fun fetchMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int): List<Message> = suspendCancellableCoroutine { continuation ->
val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass) context.feature(Messaging::class).conversationManager?.fetchConversationWithMessagesPaginated(conversationId,
.override("onFetchConversationWithMessagesComplete") { param ->
val messagesList = param.arg<List<*>>(1).map { Message(it) }
continuation.resumeWith(Result.success(messagesList))
}
.override("onServerRequest", shouldUnhook = false) {}
.override("onError") {
continuation.resumeWith(Result.failure(Exception("Failed to fetch messages")))
}.build()
fetchConversationWithMessagesPaginatedMethod.invoke(
context.feature(Messaging::class).conversationManager,
SnapUUID.fromString(conversationId).instanceNonNull(),
lastMessageId, lastMessageId,
amount, amount, onSuccess = { messages ->
callback continuation.resumeWith(Result.success(messages))
) }, onError = {
continuation.resumeWith(Result.success(emptyList()))
}) ?: continuation.resumeWith(Result.success(emptyList()))
} }
private suspend fun exportFullConversation(friendFeedEntry: FriendFeedEntry) { private suspend fun exportFullConversation(friendFeedEntry: FriendFeedEntry) {

View File

@ -9,6 +9,7 @@ class SnapClassCache (
val presenceSession by lazy { findClass("com.snapchat.talkcorev3.PresenceSession\$CppProxy") } val presenceSession by lazy { findClass("com.snapchat.talkcorev3.PresenceSession\$CppProxy") }
val message by lazy { findClass("com.snapchat.client.messaging.Message") } val message by lazy { findClass("com.snapchat.client.messaging.Message") }
val messageUpdateEnum by lazy { findClass("com.snapchat.client.messaging.MessageUpdate") } val messageUpdateEnum by lazy { findClass("com.snapchat.client.messaging.MessageUpdate") }
val serverMessageIdentifier by lazy { findClass("com.snapchat.client.messaging.ServerMessageIdentifier") }
val unifiedGrpcService by lazy { findClass("com.snapchat.client.grpc.UnifiedGrpcService\$CppProxy") } val unifiedGrpcService by lazy { findClass("com.snapchat.client.grpc.UnifiedGrpcService\$CppProxy") }
val networkApi by lazy { findClass("com.snapchat.client.network_api.NetworkApi\$CppProxy") } val networkApi by lazy { findClass("com.snapchat.client.network_api.NetworkApi\$CppProxy") }
val messageDestinations by lazy { findClass("com.snapchat.client.messaging.MessageDestinations") } val messageDestinations by lazy { findClass("com.snapchat.client.messaging.MessageDestinations") }

View File

@ -1,13 +1,13 @@
package me.rhunk.snapenhance.core.features.impl.messaging package me.rhunk.snapenhance.core.features.impl.messaging
import me.rhunk.snapenhance.common.data.MessageState import me.rhunk.snapenhance.common.data.MessageState
import me.rhunk.snapenhance.common.data.MessageUpdate
import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.common.data.MessagingRuleType
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.MessagingRuleFeature
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
import me.rhunk.snapenhance.core.logger.CoreLogger import me.rhunk.snapenhance.core.logger.CoreLogger
import me.rhunk.snapenhance.core.util.CallbackBuilder
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.Hooker
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
@ -21,14 +21,6 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE,
private val messageLogger by lazy { context.feature(MessageLogger::class) } private val messageLogger by lazy { context.feature(MessageLogger::class) }
private val messaging by lazy { context.feature(Messaging::class) } private val messaging by lazy { context.feature(Messaging::class) }
private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") }
private val callbackClass by lazy { context.mappings.getMappedClass("callbacks", "Callback") }
private val updateMessageMethod by lazy { context.classCache.conversationManager.methods.first { it.name == "updateMessage" } }
private val fetchConversationWithMessagesPaginatedMethod by lazy {
context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" }
}
private val autoSaveFilter by lazy { private val autoSaveFilter by lazy {
context.config.messaging.autoSaveMessagesInConversations.get() context.config.messaging.autoSaveMessagesInConversations.get()
} }
@ -39,20 +31,17 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE,
if (message.messageState != MessageState.COMMITTED) return if (message.messageState != MessageState.COMMITTED) return
runCatching { runCatching {
val callback = CallbackBuilder(callbackClass) context.feature(Messaging::class).conversationManager?.updateMessage(
.override("onError") { conversationId.toString(),
context.log.warn("Error saving message $messageId")
}.build()
updateMessageMethod.invoke(
context.feature(Messaging::class).conversationManager,
conversationId.instanceNonNull(),
messageId, messageId,
context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == "SAVE" }, MessageUpdate.SAVE
callback ) {
) if (it != null) {
context.log.warn("Error saving message $messageId: $it")
}
}
}.onFailure { }.onFailure {
CoreLogger.xposedLog("Error saving message $messageId", it) context.log.error("Error saving message $messageId", it)
} }
//delay between saves //delay between saves
@ -60,6 +49,7 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE,
} }
private fun canSaveMessage(message: Message): Boolean { private fun canSaveMessage(message: Message): Boolean {
if (context.mainActivity == null || context.isMainActivityPaused) return false
if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == context.database.myUserId }) return false if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == context.database.myUserId }) return false
val contentType = message.messageContent.contentType.toString() val contentType = message.messageContent.contentType.toString()
@ -121,14 +111,14 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE,
HookStage.BEFORE, HookStage.BEFORE,
{ autoSaveFilter.isNotEmpty() } { autoSaveFilter.isNotEmpty() }
) { ) {
val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass).build()
val conversationUUID = messaging.openedConversationUUID ?: return@hook val conversationUUID = messaging.openedConversationUUID ?: return@hook
runCatching { runCatching {
fetchConversationWithMessagesPaginatedMethod.invoke( messaging.conversationManager?.fetchConversationWithMessagesPaginated(
messaging.conversationManager, conversationUUID.instanceNonNull(), conversationUUID.toString(),
Long.MAX_VALUE, Long.MAX_VALUE,
10, 10,
callback onSuccess = {},
onError = {}
) )
}.onFailure { }.onFailure {
CoreLogger.xposedLog("failed to save message", it) CoreLogger.xposedLog("failed to save message", it)

View File

@ -9,12 +9,13 @@ import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.Hooker
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.wrapper.impl.ConversationManager
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) { class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) {
private var _conversationManager: Any? = null var conversationManager: ConversationManager? = null
val conversationManager: Any? private set
get() = _conversationManager
var openedConversationUUID: SnapUUID? = null var openedConversationUUID: SnapUUID? = null
private set private set
@ -28,7 +29,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
override fun init() { override fun init() {
Hooker.hookConstructor(context.classCache.conversationManager, HookStage.BEFORE) { param -> Hooker.hookConstructor(context.classCache.conversationManager, HookStage.BEFORE) { param ->
_conversationManager = param.thisObject() conversationManager = ConversationManager(context, param.thisObject())
context.messagingBridge.triggerSessionStart() context.messagingBridge.triggerSessionStart()
context.mainActivity?.takeIf { it.intent.getBooleanExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA,false) }?.run { context.mainActivity?.takeIf { it.intent.getBooleanExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA,false) }?.run {
finishAndRemoveTask() finishAndRemoveTask()

View File

@ -14,6 +14,7 @@ import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.MediaReferenceType import me.rhunk.snapenhance.common.data.MediaReferenceType
import me.rhunk.snapenhance.common.data.MessageUpdate
import me.rhunk.snapenhance.common.data.NotificationType import me.rhunk.snapenhance.common.data.NotificationType
import me.rhunk.snapenhance.common.data.download.SplitMediaAssetType import me.rhunk.snapenhance.common.data.download.SplitMediaAssetType
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
@ -191,31 +192,26 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
val conversationManager = context.feature(Messaging::class).conversationManager ?: return@subscribe val conversationManager = context.feature(Messaging::class).conversationManager ?: return@subscribe
context.classCache.conversationManager.methods.first { it.name == "displayedMessages"}?.invoke( conversationManager.displayedMessages(
conversationManager, conversationId,
SnapUUID.fromString(conversationId).instanceNonNull(),
messageId, messageId,
CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) onResult = {
.override("onError") { if (it != null) {
context.log.error("Failed to mark message as read: ${it.arg(0) as Any}") context.log.error("Failed to mark conversation as read: $it")
context.shortToast("Failed to mark message as read") context.shortToast("Failed to mark conversation as read")
}.build() }
}
) )
val conversationMessage = context.database.getConversationMessageFromId(messageId) ?: return@subscribe val conversationMessage = context.database.getConversationMessageFromId(messageId) ?: return@subscribe
if (conversationMessage.contentType == ContentType.SNAP.id) { if (conversationMessage.contentType == ContentType.SNAP.id) {
context.classCache.conversationManager.methods.first { it.name == "updateMessage"}?.invoke( conversationManager.updateMessage(conversationId, messageId, MessageUpdate.READ) {
conversationManager, if (it != null) {
SnapUUID.fromString(conversationId).instanceNonNull(), context.log.error("Failed to open snap: $it")
messageId, context.shortToast("Failed to open snap")
context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == "READ" }, }
CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) }
.override("onError") {
context.log.error("Failed to open snap: ${it.arg(0) as Any}")
context.shortToast("Failed to open snap")
}.build()
)
} }
}.onFailure { }.onFailure {
context.log.error("Failed to mark message as read", it) context.log.error("Failed to mark message as read", it)
@ -346,8 +342,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
override fun init() { override fun init() {
setupBroadcastReceiverHook() setupBroadcastReceiverHook()
val fetchConversationWithMessagesCallback = context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")
notifyAsUserMethod.hook(HookStage.BEFORE) { param -> notifyAsUserMethod.hook(HookStage.BEFORE) { param ->
val notificationData = NotificationData(param.argNullable(0), param.arg(1), param.arg(2), param.arg(3)) val notificationData = NotificationData(param.argNullable(0), param.arg(1), param.arg(2), param.arg(3))
@ -361,22 +355,16 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
notificationType.contains(it) notificationType.contains(it)
}) return@hook }) return@hook
val conversationManager: Any = context.feature(Messaging::class).conversationManager ?: return@hook
synchronized(notificationDataQueue) { synchronized(notificationDataQueue) {
notificationDataQueue[messageId.toLong()] = notificationData notificationDataQueue[messageId.toLong()] = notificationData
} }
val callback = CallbackBuilder(fetchConversationWithMessagesCallback) context.feature(Messaging::class).conversationManager?.fetchConversationWithMessages(conversationId, onSuccess = { messages ->
.override("onFetchConversationWithMessagesComplete") { callbackParam -> fetchMessagesResult(conversationId, messages)
val messageList = (callbackParam.arg(1) as List<Any>).map { msg -> Message(msg) } }, onError = {
fetchMessagesResult(conversationId, messageList) context.log.error("Failed to fetch conversation with messages: $it")
} })
.override("onError") {
context.log.error("Failed to fetch message ${it.arg(0) as Any}")
}.build()
fetchConversationWithMessagesMethod.invoke(conversationManager, SnapUUID.fromString(conversationId).instanceNonNull(), callback)
param.setResult(null) param.setResult(null)
} }

View File

@ -5,11 +5,10 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
import me.rhunk.snapenhance.bridge.snapclient.SessionStartListener import me.rhunk.snapenhance.bridge.snapclient.SessionStartListener
import me.rhunk.snapenhance.bridge.snapclient.types.Message import me.rhunk.snapenhance.bridge.snapclient.types.Message
import me.rhunk.snapenhance.common.data.MessageUpdate
import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.util.CallbackBuilder
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
fun me.rhunk.snapenhance.core.wrapper.impl.Message.toBridge(): Message { fun me.rhunk.snapenhance.core.wrapper.impl.Message.toBridge(): Message {
@ -46,23 +45,14 @@ class CoreMessagingBridge(
override fun fetchMessage(conversationId: String, clientMessageId: String): Message? { override fun fetchMessage(conversationId: String, clientMessageId: String): Message? {
return runBlocking { return runBlocking {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
val callback = CallbackBuilder( conversationManager?.fetchMessage(
context.mappings.getMappedClass("callbacks", "FetchMessageCallback") conversationId,
).override("onFetchMessageComplete") { param ->
val message = me.rhunk.snapenhance.core.wrapper.impl.Message(param.arg(0)).toBridge()
continuation.resumeWith(Result.success(message))
}
.override("onServerRequest", shouldUnhook = false) {}
.override("onError") {
continuation.resumeWith(Result.success(null))
}.build()
context.classCache.conversationManager.methods.first { it.name == "fetchMessage" }.invoke(
conversationManager,
SnapUUID.fromString(conversationId).instanceNonNull(),
clientMessageId.toLong(), clientMessageId.toLong(),
callback onSuccess = {
) continuation.resumeWith(Result.success(it.toBridge()))
},
onError = { continuation.resumeWith(Result.success(null)) }
) ?: continuation.resumeWith(Result.success(null))
} }
} }
} }
@ -73,26 +63,14 @@ class CoreMessagingBridge(
): Message? { ): Message? {
return runBlocking { return runBlocking {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
val callback = CallbackBuilder( conversationManager?.fetchMessageByServerId(
context.mappings.getMappedClass("callbacks", "FetchMessageCallback") conversationId,
).override("onFetchMessageComplete") { param -> serverMessageId,
val message = me.rhunk.snapenhance.core.wrapper.impl.Message(param.arg(1)).toBridge() onSuccess = {
continuation.resumeWith(Result.success(message)) continuation.resumeWith(Result.success(it.toBridge()))
} },
.override("onServerRequest", shouldUnhook = false) {} onError = { continuation.resumeWith(Result.success(null)) }
.override("onError") { ) ?: continuation.resumeWith(Result.success(null))
continuation.resumeWith(Result.success(null))
}.build()
val serverMessageIdentifier = context.androidContext.classLoader.loadClass("com.snapchat.client.messaging.ServerMessageIdentifier")
.getConstructor(context.classCache.snapUUID, Long::class.javaPrimitiveType)
.newInstance(SnapUUID.fromString(conversationId).instanceNonNull(), serverMessageId.toLong())
context.classCache.conversationManager.methods.first { it.name == "fetchMessageByServerId" }.invoke(
conversationManager,
serverMessageIdentifier,
callback
)
} }
} }
} }
@ -104,26 +82,17 @@ class CoreMessagingBridge(
): List<Message>? { ): List<Message>? {
return runBlocking { return runBlocking {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
val callback = CallbackBuilder( conversationManager?.fetchConversationWithMessagesPaginated(
context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") conversationId,
).override("onFetchConversationWithMessagesComplete") { param ->
val messagesList = param.arg<List<*>>(1).map {
me.rhunk.snapenhance.core.wrapper.impl.Message(it).toBridge()
}
continuation.resumeWith(Result.success(messagesList))
}
.override("onServerRequest", shouldUnhook = false) {}
.override("onError") {
continuation.resumeWith(Result.success(null))
}.build()
context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" }.invoke(
conversationManager,
SnapUUID.fromString(conversationId).instanceNonNull(),
beforeMessageId, beforeMessageId,
limit, limit,
callback onSuccess = { messages ->
) continuation.resumeWith(Result.success(messages.map { it.toBridge() }))
},
onError = {
continuation.resumeWith(Result.success(null))
}
) ?: continuation.resumeWith(Result.success(null))
} }
} }
} }
@ -135,22 +104,14 @@ class CoreMessagingBridge(
): String? { ): String? {
return runBlocking { return runBlocking {
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
val callback = CallbackBuilder( conversationManager?.updateMessage(
context.mappings.getMappedClass("callbacks", "Callback") conversationId,
).override("onSuccess") {
continuation.resumeWith(Result.success(null))
}
.override("onError") {
continuation.resumeWith(Result.success(it.arg<Any>(0).toString()))
}.build()
context.classCache.conversationManager.methods.first { it.name == "updateMessage" }.invoke(
conversationManager,
SnapUUID.fromString(conversationId).instanceNonNull(),
clientMessageId, clientMessageId,
context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == messageUpdate }, MessageUpdate.valueOf(messageUpdate),
callback onResult = {
) continuation.resumeWith(Result.success(it))
}
) ?: continuation.resumeWith(Result.success("ConversationManager is null"))
} }
} }
} }

View File

@ -26,10 +26,10 @@ class CallbackBuilder(
fun build(): Any { fun build(): Any {
//get the first param of the first constructor to get the class of the invoker //get the first param of the first constructor to get the class of the invoker
val invokerClass: Class<*> = callbackClass.constructors[0].parameterTypes[0] val rxEmitter: Class<*> = callbackClass.constructors[0].parameterTypes[0]
//get the invoker field based on the invoker class //get the emitter field based on the class
val invokerField = callbackClass.fields.first { field: Field -> val rxEmitterField = callbackClass.fields.first { field: Field ->
field.type.isAssignableFrom(invokerClass) field.type.isAssignableFrom(rxEmitter)
} }
//get the callback field based on the callback class //get the callback field based on the callback class
val callbackInstance = createEmptyObject(callbackClass.constructors[0])!! val callbackInstance = createEmptyObject(callbackClass.constructors[0])!!
@ -44,8 +44,8 @@ class CallbackBuilder(
//default hook that unhooks the callback and returns null //default hook that unhooks the callback and returns null
val defaultHook: (HookAdapter) -> Boolean = defaultHook@{ val defaultHook: (HookAdapter) -> Boolean = defaultHook@{
//checking invokerField ensure that's the callback was created by the CallbackBuilder //ensure that's the callback was created by the CallbackBuilder
if (invokerField.get(it.thisObject()) != null) return@defaultHook false if (rxEmitterField.get(it.thisObject()) != null) return@defaultHook false
if ((it.thisObject() as Any).hashCode() != callbackInstanceHashCode) return@defaultHook false if ((it.thisObject() as Any).hashCode() != callbackInstanceHashCode) return@defaultHook false
it.setResult(null) it.setResult(null)
true true

View File

@ -1,119 +0,0 @@
package me.rhunk.snapenhance.core.util
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.Arrays
import java.util.Objects
object ReflectionHelper {
/**
* Searches for a field with a class that has a method with the specified name
*/
fun searchFieldWithClassMethod(clazz: Class<*>, methodName: String): Field? {
return clazz.declaredFields.firstOrNull { f: Field? ->
try {
return@firstOrNull Arrays.stream(
f!!.type.declaredMethods
).anyMatch { method: Method -> method.name == methodName }
} catch (e: Exception) {
return@firstOrNull false
}
}
}
fun searchFieldByType(clazz: Class<*>, type: Class<*>): Field? {
return clazz.declaredFields.firstOrNull { f: Field? -> f!!.type == type }
}
fun searchFieldTypeInSuperClasses(clazz: Class<*>, type: Class<*>): Field? {
val field = searchFieldByType(clazz, type)
if (field != null) {
return field
}
val superclass = clazz.superclass
return superclass?.let { searchFieldTypeInSuperClasses(it, type) }
}
fun searchFieldStartsWithToString(
clazz: Class<*>,
instance: Any,
toString: String?
): Field? {
return clazz.declaredFields.firstOrNull { f: Field ->
try {
f.isAccessible = true
return@firstOrNull Objects.requireNonNull(f[instance]).toString()
.startsWith(
toString!!
)
} catch (e: Throwable) {
return@firstOrNull false
}
}
}
fun searchFieldContainsToString(
clazz: Class<*>,
instance: Any?,
toString: String?
): Field? {
return clazz.declaredFields.firstOrNull { f: Field ->
try {
f.isAccessible = true
return@firstOrNull Objects.requireNonNull(f[instance]).toString()
.contains(toString!!)
} catch (e: Throwable) {
return@firstOrNull false
}
}
}
fun searchFirstFieldTypeInClassRecursive(clazz: Class<*>, type: Class<*>): Field? {
return clazz.declaredFields.firstOrNull {
val field = searchFieldByType(it.type, type)
return@firstOrNull field != null
}
}
/**
* Searches for a field with a class that has a method with the specified return type
*/
fun searchMethodWithReturnType(clazz: Class<*>, returnType: Class<*>): Method? {
return clazz.declaredMethods.first { m: Method -> m.returnType == returnType }
}
/**
* Searches for a field with a class that has a method with the specified return type and parameter types
*/
fun searchMethodWithParameterAndReturnType(
aClass: Class<*>,
returnType: Class<*>,
vararg parameters: Class<*>
): Method? {
return aClass.declaredMethods.firstOrNull { m: Method ->
if (m.returnType != returnType) {
return@firstOrNull false
}
val parameterTypes = m.parameterTypes
if (parameterTypes.size != parameters.size) {
return@firstOrNull false
}
for (i in parameterTypes.indices) {
if (parameterTypes[i] != parameters[i]) {
return@firstOrNull false
}
}
true
}
}
fun getDeclaredFieldsRecursively(clazz: Class<*>): List<Field> {
val fields = clazz.declaredFields.toMutableList()
val superclass = clazz.superclass
if (superclass != null) {
fields.addAll(getDeclaredFieldsRecursively(superclass))
}
return fields
}
}

View File

@ -0,0 +1,105 @@
package me.rhunk.snapenhance.core.wrapper.impl
import me.rhunk.snapenhance.common.data.MessageUpdate
import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.util.CallbackBuilder
import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
typealias CallbackResult = (error: String?) -> Unit
class ConversationManager(val context: ModContext, obj: Any) : AbstractWrapper(obj) {
private fun findMethodByName(name: String) = context.classCache.conversationManager.declaredMethods.find { it.name == name } ?: throw RuntimeException("Could not find method $name")
private val updateMessageMethod by lazy { findMethodByName("updateMessage") }
private val fetchConversationWithMessagesPaginatedMethod by lazy { findMethodByName("fetchConversationWithMessagesPaginated") }
private val fetchConversationWithMessagesMethod by lazy { findMethodByName("fetchConversationWithMessages") }
private val fetchMessageByServerId by lazy { findMethodByName("fetchMessageByServerId") }
private val displayedMessagesMethod by lazy { findMethodByName("displayedMessages") }
private val fetchMessage by lazy { findMethodByName("fetchMessage") }
fun updateMessage(conversationId: String, messageId: Long, action: MessageUpdate, onResult: CallbackResult = {}) {
updateMessageMethod.invoke(
instanceNonNull(),
SnapUUID.fromString(conversationId).instanceNonNull(),
messageId,
context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == action.toString() },
CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback"))
.override("onSuccess") { onResult(null) }
.override("onError") { onResult(it.arg<Any>(0).toString()) }.build()
)
}
fun fetchConversationWithMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int, onSuccess: (message: List<Message>) -> Unit, onError: (error: String) -> Unit) {
val callback = CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback"))
.override("onFetchConversationWithMessagesComplete") { param ->
onSuccess(param.arg<List<*>>(1).map { Message(it) })
}
.override("onServerRequest", shouldUnhook = false) {}
.override("onError") {
onError(it.arg<Any>(0).toString())
}.build()
fetchConversationWithMessagesPaginatedMethod.invoke(instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), lastMessageId, amount, callback)
}
fun fetchConversationWithMessages(conversationId: String, onSuccess: (List<Message>) -> Unit, onError: (error: String) -> Unit) {
fetchConversationWithMessagesMethod.invoke(
instanceNonNull(),
conversationId.toSnapUUID().instanceNonNull(),
CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback"))
.override("onFetchConversationWithMessagesComplete") { param ->
onSuccess(param.arg<List<*>>(1).map { Message(it) })
}
.override("onServerRequest", shouldUnhook = false) {}
.override("onError") {
onError(it.arg<Any>(0).toString())
}.build()
)
}
fun displayedMessages(conversationId: String, messageId: Long, onResult: CallbackResult = {}) {
displayedMessagesMethod.invoke(
instanceNonNull(),
conversationId.toSnapUUID(),
messageId,
CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback"))
.override("onSuccess") { onResult(null) }
.override("onError") { onResult(it.arg<Any>(0).toString()) }.build()
)
}
fun fetchMessage(conversationId: String, messageId: Long, onSuccess: (Message) -> Unit, onError: (error: String) -> Unit = {}) {
fetchMessage.invoke(
instanceNonNull(),
conversationId.toSnapUUID().instanceNonNull(),
messageId,
CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback"))
.override("onSuccess") { param ->
onSuccess(Message(param.arg(0)))
}
.override("onServerRequest", shouldUnhook = false) {}
.override("onError") {
onError(it.arg<Any>(0).toString())
}.build()
)
}
fun fetchMessageByServerId(conversationId: String, serverMessageId: String, onSuccess: (Message) -> Unit, onError: (error: String) -> Unit) {
val serverMessageIdentifier = context.classCache.serverMessageIdentifier
.getConstructor(context.classCache.snapUUID, Long::class.javaPrimitiveType)
.newInstance(conversationId.toSnapUUID().instanceNonNull(), serverMessageId.toLong())
fetchMessageByServerId.invoke(
instanceNonNull(),
serverMessageIdentifier,
CallbackBuilder(context.mappings.getMappedClass("callbacks", "FetchMessageCallback"))
.override("onFetchMessageComplete") { param ->
onSuccess(Message(param.arg(1)))
}
.override("onServerRequest", shouldUnhook = false) {}
.override("onError") {
onError(it.arg<Any>(0).toString())
}.build()
)
}
}

View File

@ -6,6 +6,8 @@ import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.UUID import java.util.UUID
fun String.toSnapUUID() = SnapUUID.fromString(this)
class SnapUUID(obj: Any?) : AbstractWrapper(obj) { class SnapUUID(obj: Any?) : AbstractWrapper(obj) {
private val uuidString by lazy { toUUID().toString() } private val uuidString by lazy { toUUID().toString() }

View File

@ -1,21 +1,19 @@
package me.rhunk.snapenhance.core.wrapper.impl.media.opera package me.rhunk.snapenhance.core.wrapper.impl.media.opera
import me.rhunk.snapenhance.core.util.ReflectionHelper import me.rhunk.snapenhance.common.util.ktx.findFieldsToString
import me.rhunk.snapenhance.core.wrapper.AbstractWrapper import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
class Layer(obj: Any?) : AbstractWrapper(obj) { class Layer(obj: Any?) : AbstractWrapper(obj) {
val paramMap: ParamMap val paramMap: ParamMap
get() { get() {
val layerControllerField = ReflectionHelper.searchFieldContainsToString( val layerControllerField = instanceNonNull()::class.java.findFieldsToString(instance, once = true) { _, value ->
instanceNonNull()::class.java, value.contains("OperaPageModel")
instance, }.firstOrNull() ?: throw RuntimeException("Could not find layerController field")
"OperaPageModel"
)!! val paramsMapHashMap = layerControllerField.type.findFieldsToString(layerControllerField[instance], once = true) { _, value ->
value.contains("OperaPageModel")
}.firstOrNull() ?: throw RuntimeException("Could not find paramsMap field")
val paramsMapHashMap = ReflectionHelper.searchFieldStartsWithToString(
layerControllerField.type,
layerControllerField[instance] as Any, "OperaPageModel"
)!!
return ParamMap(paramsMapHashMap[layerControllerField[instance]]!!) return ParamMap(paramsMapHashMap[layerControllerField[instance]]!!)
} }
} }

View File

@ -1,18 +0,0 @@
package me.rhunk.snapenhance.core.wrapper.impl.media.opera
import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.core.util.ReflectionHelper
import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap
class LayerController(obj: Any?) : AbstractWrapper(obj) {
val paramMap: ParamMap
get() {
val paramMapField: Field = ReflectionHelper.searchFieldTypeInSuperClasses(
instanceNonNull()::class.java,
ConcurrentHashMap::class.java
) ?: throw RuntimeException("Could not find paramMap field")
return ParamMap(XposedHelpers.getObjectField(instance, paramMapField.name))
}
}

View File

@ -1,6 +1,6 @@
package me.rhunk.snapenhance.core.wrapper.impl.media.opera package me.rhunk.snapenhance.core.wrapper.impl.media.opera
import me.rhunk.snapenhance.core.util.ReflectionHelper import me.rhunk.snapenhance.common.util.ktx.findFields
import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.wrapper.AbstractWrapper import me.rhunk.snapenhance.core.wrapper.AbstractWrapper
import java.lang.reflect.Field import java.lang.reflect.Field
@ -9,10 +9,9 @@ import java.util.concurrent.ConcurrentHashMap
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class ParamMap(obj: Any?) : AbstractWrapper(obj) { class ParamMap(obj: Any?) : AbstractWrapper(obj) {
private val paramMapField: Field by lazy { private val paramMapField: Field by lazy {
ReflectionHelper.searchFieldTypeInSuperClasses( instanceNonNull()::class.java.findFields(once = true) {
instanceNonNull().javaClass, it.type == ConcurrentHashMap::class.java
ConcurrentHashMap::class.java }.firstOrNull() ?: throw RuntimeException("Could not find paramMap field")
)!!
} }
val concurrentHashMap: ConcurrentHashMap<Any, Any> val concurrentHashMap: ConcurrentHashMap<Any, Any>