mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-22 19:08:58 +02:00
refactor: conversation manager wrapper
- fix auto save in background
This commit is contained in:
parent
8d1c9a87ad
commit
17f81eb682
@ -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"),
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)!!
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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") }
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
@ -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() }
|
||||
|
||||
|
@ -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]]!!)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user