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
}
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"),

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() }
var isMainActivityPaused = false
fun <T : Feature> feature(featureClass: KClass<T>): T {
return features.get(featureClass)!!
}

View File

@ -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()

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.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) {

View File

@ -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") }

View File

@ -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)

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.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()

View File

@ -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}")
conversationManager.updateMessage(conversationId, messageId, MessageUpdate.READ) {
if (it != null) {
context.log.error("Failed to open snap: $it")
context.shortToast("Failed to open snap")
}.build()
)
}
}
}
}.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)
}

View File

@ -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"))
}
}
}

View File

@ -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

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.util.UUID
fun String.toSnapUUID() = SnapUUID.fromString(this)
class SnapUUID(obj: Any?) : AbstractWrapper(obj) {
private val uuidString by lazy { toUUID().toString() }

View File

@ -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]]!!)
}
}

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
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>