commit 17f81eb6827e1a966cd6dd19b9d0d6ced095b3e0 parent 8d1c9a87ad13dd8d01641cc8d0fad33ab6c6e32e Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 2 Nov 2023 14:08:02 +0100 refactor: conversation manager wrapper - fix auto save in background Diffstat:
18 files changed, 252 insertions(+), 327 deletions(-)
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/SnapEnums.kt @@ -105,6 +105,11 @@ enum class MediaReferenceType { 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) { MUTUAL(0, "mutual"), OUTGOING(1, "outgoing"), diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/util/ktx/HashCode.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/ktx/HashCode.kt @@ -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 -}- \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/util/ktx/JavaExt.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/util/ktx/JavaExt.kt @@ -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 + } + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt @@ -67,6 +67,8 @@ class ModContext( val isDeveloper by lazy { config.scripting.developerMode.get() } + var isMainActivityPaused = false + fun <T : Feature> feature(featureClass: KClass<T>): T { return features.get(featureClass)!! } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -35,7 +35,6 @@ class SnapEnhance { } private lateinit var appContext: ModContext private var isBridgeInitialized = false - private var isActivityPaused = false private fun hookMainActivity(methodName: String, stage: HookStage = HookStage.AFTER, block: Activity.() -> Unit) { Activity::class.java.hook(methodName, stage, { isBridgeInitialized }) { param -> @@ -91,14 +90,14 @@ class SnapEnhance { hookMainActivity("onPause") { appContext.bridgeClient.closeSettingsOverlay() - isActivityPaused = true + appContext.isMainActivityPaused = true } var activityWasResumed = false //we need to reload the config when the app is resumed //FIXME: called twice at first launch hookMainActivity("onResume") { - isActivityPaused = false + appContext.isMainActivityPaused = false if (!activityWasResumed) { activityWasResumed = true return@hookMainActivity @@ -175,7 +174,7 @@ class SnapEnhance { } fun runLater(task: () -> Unit) { - if (isActivityPaused) { + if (appContext.isMainActivityPaused) { tasks.add(task) } else { task() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/ExportChatMessages.kt @@ -18,18 +18,11 @@ import me.rhunk.snapenhance.core.logger.CoreLogger import me.rhunk.snapenhance.core.messaging.ExportFormat import me.rhunk.snapenhance.core.messaging.MessageExporter 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.SnapUUID import java.io.File import kotlin.math.absoluteValue 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 var currentActionDialog: AlertDialog? = null @@ -149,24 +142,14 @@ class ExportChatMessages : AbstractAction() { } } - private suspend fun fetchMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int) = suspendCancellableCoroutine { continuation -> - val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass) - .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(), + private suspend fun fetchMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int): List<Message> = suspendCancellableCoroutine { continuation -> + context.feature(Messaging::class).conversationManager?.fetchConversationWithMessagesPaginated(conversationId, lastMessageId, - amount, - callback - ) + amount, onSuccess = { messages -> + continuation.resumeWith(Result.success(messages)) + }, onError = { + continuation.resumeWith(Result.success(emptyList())) + }) ?: continuation.resumeWith(Result.success(emptyList())) } private suspend fun exportFullConversation(friendFeedEntry: FriendFeedEntry) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/data/SnapClassCache.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/data/SnapClassCache.kt @@ -9,6 +9,7 @@ class SnapClassCache ( val presenceSession by lazy { findClass("com.snapchat.talkcorev3.PresenceSession\$CppProxy") } val message by lazy { findClass("com.snapchat.client.messaging.Message") } 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 networkApi by lazy { findClass("com.snapchat.client.network_api.NetworkApi\$CppProxy") } val messageDestinations by lazy { findClass("com.snapchat.client.messaging.MessageDestinations") } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/AutoSave.kt @@ -1,13 +1,13 @@ package me.rhunk.snapenhance.core.features.impl.messaging 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.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger import me.rhunk.snapenhance.core.features.impl.spying.StealthMode 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.Hooker 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 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 { context.config.messaging.autoSaveMessagesInConversations.get() } @@ -39,20 +31,17 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, if (message.messageState != MessageState.COMMITTED) return runCatching { - val callback = CallbackBuilder(callbackClass) - .override("onError") { - context.log.warn("Error saving message $messageId") - }.build() - - updateMessageMethod.invoke( - context.feature(Messaging::class).conversationManager, - conversationId.instanceNonNull(), + context.feature(Messaging::class).conversationManager?.updateMessage( + conversationId.toString(), messageId, - context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == "SAVE" }, - callback - ) + MessageUpdate.SAVE + ) { + if (it != null) { + context.log.warn("Error saving message $messageId: $it") + } + } }.onFailure { - CoreLogger.xposedLog("Error saving message $messageId", it) + context.log.error("Error saving message $messageId", it) } //delay between saves @@ -60,6 +49,7 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, } 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 val contentType = message.messageContent.contentType.toString() @@ -121,14 +111,14 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, HookStage.BEFORE, { autoSaveFilter.isNotEmpty() } ) { - val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass).build() val conversationUUID = messaging.openedConversationUUID ?: return@hook runCatching { - fetchConversationWithMessagesPaginatedMethod.invoke( - messaging.conversationManager, conversationUUID.instanceNonNull(), + messaging.conversationManager?.fetchConversationWithMessagesPaginated( + conversationUUID.toString(), Long.MAX_VALUE, 10, - callback + onSuccess = {}, + onError = {} ) }.onFailure { CoreLogger.xposedLog("failed to save message", it) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt @@ -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.hook import me.rhunk.snapenhance.core.util.ktx.getObjectField +import me.rhunk.snapenhance.core.wrapper.impl.ConversationManager 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) { - private var _conversationManager: Any? = null - val conversationManager: Any? - get() = _conversationManager + var conversationManager: ConversationManager? = null + private set + var openedConversationUUID: SnapUUID? = null private set @@ -28,7 +29,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C override fun init() { Hooker.hookConstructor(context.classCache.conversationManager, HookStage.BEFORE) { param -> - _conversationManager = param.thisObject() + conversationManager = ConversationManager(context, param.thisObject()) context.messagingBridge.triggerSessionStart() context.mainActivity?.takeIf { it.intent.getBooleanExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA,false) }?.run { finishAndRemoveTask() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Notifications.kt @@ -14,6 +14,7 @@ import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers import me.rhunk.snapenhance.common.data.ContentType 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.download.SplitMediaAssetType 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 - context.classCache.conversationManager.methods.first { it.name == "displayedMessages"}?.invoke( - conversationManager, - SnapUUID.fromString(conversationId).instanceNonNull(), + conversationManager.displayedMessages( + conversationId, messageId, - CallbackBuilder(context.mappings.getMappedClass("callbacks", "Callback")) - .override("onError") { - context.log.error("Failed to mark message as read: ${it.arg(0) as Any}") - context.shortToast("Failed to mark message as read") - }.build() + onResult = { + if (it != null) { + context.log.error("Failed to mark conversation as read: $it") + context.shortToast("Failed to mark conversation as read") + } + } ) val conversationMessage = context.database.getConversationMessageFromId(messageId) ?: return@subscribe if (conversationMessage.contentType == ContentType.SNAP.id) { - context.classCache.conversationManager.methods.first { it.name == "updateMessage"}?.invoke( - conversationManager, - SnapUUID.fromString(conversationId).instanceNonNull(), - messageId, - 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() - ) + conversationManager.updateMessage(conversationId, messageId, MessageUpdate.READ) { + if (it != null) { + context.log.error("Failed to open snap: $it") + context.shortToast("Failed to open snap") + } + } } }.onFailure { context.log.error("Failed to mark message as read", it) @@ -346,8 +342,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN override fun init() { setupBroadcastReceiverHook() - val fetchConversationWithMessagesCallback = context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") - notifyAsUserMethod.hook(HookStage.BEFORE) { param -> 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) }) return@hook - val conversationManager: Any = context.feature(Messaging::class).conversationManager ?: return@hook - synchronized(notificationDataQueue) { notificationDataQueue[messageId.toLong()] = notificationData } - val callback = CallbackBuilder(fetchConversationWithMessagesCallback) - .override("onFetchConversationWithMessagesComplete") { callbackParam -> - val messageList = (callbackParam.arg(1) as List<Any>).map { msg -> Message(msg) } - fetchMessagesResult(conversationId, messageList) - } - .override("onError") { - context.log.error("Failed to fetch message ${it.arg(0) as Any}") - }.build() + context.feature(Messaging::class).conversationManager?.fetchConversationWithMessages(conversationId, onSuccess = { messages -> + fetchMessagesResult(conversationId, messages) + }, onError = { + context.log.error("Failed to fetch conversation with messages: $it") + }) - fetchConversationWithMessagesMethod.invoke(conversationManager, SnapUUID.fromString(conversationId).instanceNonNull(), callback) param.setResult(null) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/CoreMessagingBridge.kt @@ -5,11 +5,10 @@ import kotlinx.coroutines.suspendCancellableCoroutine import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge import me.rhunk.snapenhance.bridge.snapclient.SessionStartListener 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.features.impl.downloader.decoder.MessageDecoder 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 { @@ -46,23 +45,14 @@ class CoreMessagingBridge( override fun fetchMessage(conversationId: String, clientMessageId: String): Message? { return runBlocking { suspendCancellableCoroutine { continuation -> - val callback = CallbackBuilder( - context.mappings.getMappedClass("callbacks", "FetchMessageCallback") - ).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(), + conversationManager?.fetchMessage( + conversationId, 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? { return runBlocking { suspendCancellableCoroutine { continuation -> - val callback = CallbackBuilder( - context.mappings.getMappedClass("callbacks", "FetchMessageCallback") - ).override("onFetchMessageComplete") { param -> - val message = me.rhunk.snapenhance.core.wrapper.impl.Message(param.arg(1)).toBridge() - continuation.resumeWith(Result.success(message)) - } - .override("onServerRequest", shouldUnhook = false) {} - .override("onError") { - 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 - ) + conversationManager?.fetchMessageByServerId( + conversationId, + serverMessageId, + onSuccess = { + continuation.resumeWith(Result.success(it.toBridge())) + }, + onError = { continuation.resumeWith(Result.success(null)) } + ) ?: continuation.resumeWith(Result.success(null)) } } } @@ -104,26 +82,17 @@ class CoreMessagingBridge( ): List<Message>? { return runBlocking { suspendCancellableCoroutine { continuation -> - val callback = CallbackBuilder( - context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") - ).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(), + conversationManager?.fetchConversationWithMessagesPaginated( + conversationId, beforeMessageId, 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? { return runBlocking { suspendCancellableCoroutine { continuation -> - val callback = CallbackBuilder( - context.mappings.getMappedClass("callbacks", "Callback") - ).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(), + conversationManager?.updateMessage( + conversationId, clientMessageId, - context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == messageUpdate }, - callback - ) + MessageUpdate.valueOf(messageUpdate), + onResult = { + continuation.resumeWith(Result.success(it)) + } + ) ?: continuation.resumeWith(Result.success("ConversationManager is null")) } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/CallbackBuilder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/CallbackBuilder.kt @@ -26,10 +26,10 @@ class CallbackBuilder( fun build(): Any { //get the first param of the first constructor to get the class of the invoker - val invokerClass: Class<*> = callbackClass.constructors[0].parameterTypes[0] - //get the invoker field based on the invoker class - val invokerField = callbackClass.fields.first { field: Field -> - field.type.isAssignableFrom(invokerClass) + val rxEmitter: Class<*> = callbackClass.constructors[0].parameterTypes[0] + //get the emitter field based on the class + val rxEmitterField = callbackClass.fields.first { field: Field -> + field.type.isAssignableFrom(rxEmitter) } //get the callback field based on the callback class val callbackInstance = createEmptyObject(callbackClass.constructors[0])!! @@ -44,8 +44,8 @@ class CallbackBuilder( //default hook that unhooks the callback and returns null val defaultHook: (HookAdapter) -> Boolean = defaultHook@{ - //checking invokerField ensure that's the callback was created by the CallbackBuilder - if (invokerField.get(it.thisObject()) != null) return@defaultHook false + //ensure that's the callback was created by the CallbackBuilder + if (rxEmitterField.get(it.thisObject()) != null) return@defaultHook false if ((it.thisObject() as Any).hashCode() != callbackInstanceHashCode) return@defaultHook false it.setResult(null) true diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ReflectionHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ReflectionHelper.kt @@ -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 - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt @@ -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() + ) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/SnapUUID.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/SnapUUID.kt @@ -6,6 +6,8 @@ import me.rhunk.snapenhance.core.wrapper.AbstractWrapper import java.nio.ByteBuffer import java.util.UUID +fun String.toSnapUUID() = SnapUUID.fromString(this) + class SnapUUID(obj: Any?) : AbstractWrapper(obj) { private val uuidString by lazy { toUUID().toString() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/media/opera/Layer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/media/opera/Layer.kt @@ -1,21 +1,19 @@ 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 class Layer(obj: Any?) : AbstractWrapper(obj) { val paramMap: ParamMap get() { - val layerControllerField = ReflectionHelper.searchFieldContainsToString( - instanceNonNull()::class.java, - instance, - "OperaPageModel" - )!! + val layerControllerField = instanceNonNull()::class.java.findFieldsToString(instance, once = true) { _, value -> + value.contains("OperaPageModel") + }.firstOrNull() ?: throw RuntimeException("Could not find layerController field") + + 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]]!!) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/media/opera/LayerController.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/media/opera/LayerController.kt @@ -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)) - } -} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/media/opera/ParamMap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/media/opera/ParamMap.kt @@ -1,6 +1,6 @@ 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.wrapper.AbstractWrapper import java.lang.reflect.Field @@ -9,10 +9,9 @@ import java.util.concurrent.ConcurrentHashMap @Suppress("UNCHECKED_CAST") class ParamMap(obj: Any?) : AbstractWrapper(obj) { private val paramMapField: Field by lazy { - ReflectionHelper.searchFieldTypeInSuperClasses( - instanceNonNull().javaClass, - ConcurrentHashMap::class.java - )!! + instanceNonNull()::class.java.findFields(once = true) { + it.type == ConcurrentHashMap::class.java + }.firstOrNull() ?: throw RuntimeException("Could not find paramMap field") } val concurrentHashMap: ConcurrentHashMap<Any, Any>