feat(manager): conversation preview (wip)

- add messaging bridge
- refactor export chat messages
This commit is contained in:
rhunk
2023-10-14 18:56:16 +02:00
parent 2b0e4ad09a
commit 3e9c97c18c
13 changed files with 486 additions and 80 deletions

View File

@ -19,6 +19,7 @@ import me.rhunk.snapenhance.core.bridge.loadFromBridge
import me.rhunk.snapenhance.core.data.SnapClassCache
import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
import me.rhunk.snapenhance.core.messaging.CoreMessagingBridge
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import kotlin.system.measureTimeMillis
@ -116,6 +117,7 @@ class SnapEnhance {
logCritical(null, throwable)
}
}
bridgeClient.registerMessagingBridge(CoreMessagingBridge(this))
reloadConfig()
actionManager.init()

View File

@ -25,16 +25,7 @@ import java.io.File
import kotlin.math.absoluteValue
class ExportChatMessages : AbstractAction() {
private val callbackClass by lazy { context.mappings.getMappedClass("callbacks", "Callback") }
private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") }
private val enterConversationMethod by lazy {
context.classCache.conversationManager.methods.first { it.name == "enterConversation" }
}
private val exitConversationMethod by lazy {
context.classCache.conversationManager.methods.first { it.name == "exitConversation" }
}
private val fetchConversationWithMessagesPaginatedMethod by lazy {
context.classCache.conversationManager.methods.first { it.name == "fetchConversationWithMessagesPaginated" }
}
@ -162,32 +153,6 @@ class ExportChatMessages : AbstractAction() {
}
}
private suspend fun conversationAction(isEntering: Boolean, conversationId: String, conversationType: String?) = suspendCancellableCoroutine { continuation ->
val callback = CallbackBuilder(callbackClass)
.override("onSuccess") { _ ->
continuation.resumeWith(Result.success(Unit))
}
.override("onError") {
continuation.resumeWith(Result.failure(Exception("Failed to ${if (isEntering) "enter" else "exit"} conversation")))
}.build()
if (isEntering) {
enterConversationMethod.invoke(
conversationManagerInstance,
SnapUUID.fromString(conversationId).instanceNonNull(),
enterConversationMethod.parameterTypes[1].enumConstants.first { it.toString() == conversationType },
callback
)
} else {
exitConversationMethod.invoke(
conversationManagerInstance,
SnapUUID.fromString(conversationId).instanceNonNull(),
Long.MAX_VALUE,
callback
)
}
}
private suspend fun fetchMessagesPaginated(conversationId: String, lastMessageId: Long, amount: Int) = suspendCancellableCoroutine { continuation ->
val callback = CallbackBuilder(fetchConversationWithMessagesCallbackClass)
.override("onFetchConversationWithMessagesComplete") { param ->
@ -213,10 +178,6 @@ class ExportChatMessages : AbstractAction() {
val conversationId = friendFeedEntry.key!!
val conversationName = friendFeedEntry.feedDisplayName ?: friendFeedEntry.friendDisplayName!!.split("|").lastOrNull() ?: "unknown"
runCatching {
conversationAction(true, conversationId, if (friendFeedEntry.feedDisplayName != null) "USERCREATEDGROUP" else "ONEONONE")
}
logDialog(context.translation.format("chat_export.exporting_message", "conversation" to conversationName))
val foundMessages = fetchMessagesPaginated(conversationId, Long.MAX_VALUE, amount = 1).toMutableList()
@ -266,10 +227,6 @@ class ExportChatMessages : AbstractAction() {
logDialog("\n" + context.translation.format("chat_export.exported_to",
"path" to outputFile.absolutePath.toString()
) + "\n")
runCatching {
conversationAction(false, conversationId, null)
}
}
private fun exportChatForConversations(conversations: List<FriendFeedEntry>) {

View File

@ -16,6 +16,7 @@ import me.rhunk.snapenhance.bridge.DownloadCallback
import me.rhunk.snapenhance.bridge.SyncCallback
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface
import me.rhunk.snapenhance.bridge.scripting.IScripting
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
import me.rhunk.snapenhance.common.BuildConfig
import me.rhunk.snapenhance.common.bridge.FileLoaderWrapper
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
@ -147,6 +148,8 @@ class BridgeClient(
fun getMessageLogger() = service.messageLogger
fun registerMessagingBridge(bridge: MessagingBridge) = service.registerMessagingBridge(bridge)
fun openSettingsOverlay() = service.openSettingsOverlay()
fun closeSettingsOverlay() = service.closeSettingsOverlay()

View File

@ -60,6 +60,10 @@ object MessageDecoder {
.toList()
}
fun getEncodedMediaReferences(messageContent: MessageContent): List<String> {
return getEncodedMediaReferences(gson.toJsonTree(messageContent.instanceNonNull()))
}
fun getMediaReferences(messageContent: JsonElement): List<JsonElement> {
return messageContent.asJsonObject.getAsJsonArray("mRemoteMediaReferences")
.asSequence()

View File

@ -0,0 +1,145 @@
package me.rhunk.snapenhance.core.messaging
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
import me.rhunk.snapenhance.bridge.snapclient.types.Message
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 {
return Message().also { output ->
output.conversationId = this.messageDescriptor.conversationId.toString()
output.senderId = this.senderId.toString()
output.clientMessageId = this.messageDescriptor.messageId
output.serverMessageId = this.orderKey
output.contentType = this.messageContent.contentType?.id ?: -1
output.content = this.messageContent.content
output.mediaReferences = MessageDecoder.getEncodedMediaReferences(this.messageContent)
}
}
class CoreMessagingBridge(
private val context: ModContext
) : MessagingBridge.Stub() {
private val conversationManager get() = context.feature(Messaging::class).conversationManager
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(1)).toBridge()
continuation.resumeWith(Result.success(message))
}
.override("onServerRequest", shouldUnhook = false) {}
.override("onError") {
continuation.resumeWith(Result.success(null))
}.build()
context.classCache.conversationManager.methods.first { it.name == "fetchMessage" }.invoke(
conversationManager,
SnapUUID.fromString(conversationId).instanceNonNull(),
clientMessageId,
callback
)
}
}
}
override fun fetchMessageByServerId(
conversationId: String,
serverMessageId: 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(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
)
}
}
}
override fun fetchConversationWithMessagesPaginated(
conversationId: String,
limit: Int,
beforeMessageId: Long
): 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(),
beforeMessageId,
limit,
callback
)
}
}
}
override fun updateMessage(
conversationId: String,
clientMessageId: String,
messageUpdate: String
): 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(),
clientMessageId,
context.classCache.messageUpdateEnum.enumConstants.first { it.toString() == messageUpdate },
callback
)
}
}
}
override fun getOneToOneConversationId(userId: String) = context.database.getConversationLinkFromUserId(userId)?.clientConversationId
}