commit 3e9c97c18c3140daed0c08748d76c20f2dc92d08 parent 2b0e4ad09ae0d954f781ba0e00ad5e658f638560 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:56:16 +0200 feat(manager): conversation preview (wip) - add messaging bridge - refactor export chat messages Diffstat:
13 files changed, 490 insertions(+), 80 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -48,7 +48,7 @@ class RemoteSideContext( val coroutineScope = CoroutineScope(Dispatchers.IO) private var _activity: WeakReference<ComponentActivity>? = null - lateinit var bridgeService: BridgeService + var bridgeService: BridgeService? = null var activity: ComponentActivity? get() = _activity?.get() @@ -158,11 +158,13 @@ class RemoteSideContext( log.debug(message.toString()) } + fun hasMessagingBridge() = bridgeService != null && bridgeService?.messagingBridge != null + fun checkForRequirements(overrideRequirements: Int? = null): Boolean { var requirements = overrideRequirements ?: 0 if(BuildConfig.DEBUG) { - var unixTime = System.currentTimeMillis() / 1000 //unix time in seconds cuz cool + val unixTime = System.currentTimeMillis() / 1000 //unix time in seconds cuz cool if(BuildConfig.BUILD_DATE + 604800 < unixTime.toInt()) { Toast.makeText(androidContext, "This SnapEnhance build has expired.", Toast.LENGTH_LONG).show(); throw RuntimeException("This build has expired. This crash is intentional.") diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.os.IBinder import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContextHolder +import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge import me.rhunk.snapenhance.common.bridge.types.BridgeFileType import me.rhunk.snapenhance.common.bridge.types.FileActionType import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper @@ -22,6 +23,11 @@ class BridgeService : Service() { private lateinit var messageLoggerWrapper: MessageLoggerWrapper private lateinit var remoteSideContext: RemoteSideContext lateinit var syncCallback: SyncCallback + var messagingBridge: MessagingBridge? = null + + override fun onDestroy() { + remoteSideContext.bridgeService = null + } override fun onBind(intent: Intent): IBinder? { remoteSideContext = SharedContextHolder.remote(this).apply { @@ -177,6 +183,9 @@ class BridgeService : Service() { override fun getE2eeInterface() = remoteSideContext.e2eeImplementation override fun getMessageLogger() = messageLoggerWrapper + override fun registerMessagingBridge(bridge: MessagingBridge) { + messagingBridge = bridge + } override fun openSettingsOverlay() { runCatching { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt @@ -220,7 +220,7 @@ class AddFriendDialog( getCurrentState = { context.modDatabase.getGroupInfo(group.conversationId) != null } ) { state -> if (state) { - context.bridgeService.triggerScopeSync(SocialScope.GROUP, group.conversationId) + context.bridgeService?.triggerScopeSync(SocialScope.GROUP, group.conversationId) } else { context.modDatabase.deleteGroup(group.conversationId) } @@ -247,7 +247,7 @@ class AddFriendDialog( getCurrentState = { context.modDatabase.getFriendInfo(friend.userId) != null } ) { state -> if (state) { - context.bridgeService.triggerScopeSync(SocialScope.FRIEND, friend.userId) + context.bridgeService?.triggerScopeSync(SocialScope.FRIEND, friend.userId) } else { context.modDatabase.deleteFriend(friend.userId) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/MessagingPreview.kt @@ -0,0 +1,191 @@ +package me.rhunk.snapenhance.ui.manager.sections.social + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import me.rhunk.snapenhance.RemoteSideContext +import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge +import me.rhunk.snapenhance.bridge.snapclient.types.Message +import me.rhunk.snapenhance.common.data.ContentType +import me.rhunk.snapenhance.common.data.SocialScope +import me.rhunk.snapenhance.common.util.protobuf.ProtoReader +import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper + +class MessagingPreview( + private val context: RemoteSideContext, + private val scope: SocialScope, + private val scopeId: String +) { + private lateinit var coroutineScope: CoroutineScope + private lateinit var messagingBridge: MessagingBridge + private lateinit var previewScrollState: LazyListState + private var conversationId: String? = null + private val messages = sortedMapOf<Long, Message>() + private var messageSize by mutableIntStateOf(0) + private var lastMessageId = Long.MAX_VALUE + + @Composable + private fun ConversationPreview() { + LazyColumn( + modifier = Modifier + .fillMaxSize(), + state = previewScrollState, + ) { + item { + if (messages.isEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(40.dp), + horizontalArrangement = Arrangement.Center + ) { + Text("No messages") + } + } + Spacer(modifier = Modifier.height(20.dp)) + + LaunchedEffect(Unit) { + if (messages.size > 0) { + fetchNewMessages() + } + } + } + items(messageSize) {index -> + val messageReader = ProtoReader(messages.entries.elementAt(index).value.content) + val contentType = ContentType.fromMessageContainer(messageReader) + + Card( + modifier = Modifier + .padding(5.dp) + ) { + Row( + modifier = Modifier + .padding(5.dp) + ) { + + Text("[$contentType] ${messageReader.getString(2, 1) ?: ""}") + } + } + } + } + } + + private fun fetchNewMessages() { + coroutineScope.launch(Dispatchers.IO) cs@{ + runCatching { + val queriedMessages = messagingBridge.fetchConversationWithMessagesPaginated( + conversationId!!, + 100, + lastMessageId + ) + + if (queriedMessages == null) { + context.shortToast("Failed to fetch messages") + return@cs + } + + coroutineScope.launch { + messages.putAll(queriedMessages.map { it.serverMessageId to it }) + messageSize = messages.size + if (queriedMessages.isNotEmpty()) { + lastMessageId = queriedMessages.first().clientMessageId + previewScrollState.scrollToItem(queriedMessages.size - 1) + } + } + }.onFailure { + context.shortToast("Failed to fetch messages: ${it.message}") + } + context.log.verbose("fetched ${messages.size} messages") + } + } + + private fun onMessagingBridgeReady() { + messagingBridge = context.bridgeService!!.messagingBridge!! + conversationId = if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId + if (conversationId == null) { + context.longToast("Failed to fetch conversation id") + return + } + fetchNewMessages() + } + + @Composable + private fun LoadingRow() { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(40.dp), + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier + .padding() + .size(30.dp), + strokeWidth = 3.dp, + color = MaterialTheme.colorScheme.primary + ) + } + } + + @Composable + fun Content() { + previewScrollState = rememberLazyListState() + coroutineScope = rememberCoroutineScope() + var isBridgeConnected by remember { mutableStateOf(false) } + var hasBridgeError by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxSize() + ) { + LaunchedEffect(Unit) { + isBridgeConnected = context.hasMessagingBridge() + if (isBridgeConnected) { + onMessagingBridgeReady() + } else { + SnapWidgetBroadcastReceiverHelper.create("wakeup") {}.also { + context.androidContext.sendBroadcast(it) + } + coroutineScope.launch(Dispatchers.IO) { + withTimeout(10000) { + while (!context.hasMessagingBridge()) { + delay(100) + } + isBridgeConnected = true + onMessagingBridgeReady() + } + }.invokeOnCompletion { + if (it != null) { + hasBridgeError = true + } + } + } + } + + if (hasBridgeError) { + Text("Failed to connect to Snapchat through bridge service") + } + + if (!isBridgeConnected && !hasBridgeError) { + LoadingRow() + } + + if (isBridgeConnected && !hasBridgeError) { + ConversationPreview() + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.RemoveRedEye import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.DeleteForever import androidx.compose.material3.* @@ -42,8 +43,7 @@ class SocialSection : Section() { companion object { const val MAIN_ROUTE = "social_route" - const val FRIEND_INFO_ROUTE = "friend_info/{id}" - const val GROUP_INFO_ROUTE = "group_info/{id}" + const val MESSAGING_PREVIEW_ROUTE = "messaging_preview/?id={id}&scope={scope}" } private var currentScopeContent: ScopeContent? = null @@ -70,12 +70,26 @@ class SocialSection : Section() { composable(scope.tabRoute) { val id = it.arguments?.getString("id") ?: return@composable remember { - ScopeContent(context, this@SocialSection, navController, scope, id).also { tab -> + ScopeContent( + context, + this@SocialSection, + navController, + scope, + id + ).also { tab -> currentScopeContent = tab } }.Content() } } + + composable(MESSAGING_PREVIEW_ROUTE) { + val id = it.arguments?.getString("id") ?: return@composable + val scope = it.arguments?.getString("scope") ?: return@composable + remember { + MessagingPreview(context, SocialScope.getByName(scope), id) + }.Content() + } } } @@ -90,13 +104,15 @@ class SocialSection : Section() { remember { AlertDialogs(context.translation) }.ConfirmDialog( title = "Are you sure you want to delete this ${scopeContent.scope.key.lowercase()}?", onDismiss = { deleteConfirmDialog = false }, - onConfirm = { scopeContent.deleteScope(coroutineScope); deleteConfirmDialog = false } + onConfirm = { + scopeContent.deleteScope(coroutineScope); deleteConfirmDialog = false + } ) } } } - if (currentRoute != MAIN_ROUTE) { + if (currentRoute == SocialScope.FRIEND.tabRoute || currentRoute == SocialScope.GROUP.tabRoute) { IconButton( onClick = { deleteConfirmDialog = true }, ) { @@ -128,7 +144,11 @@ class SocialSection : Section() { if (listSize == 0) { item { - Text(text = "(empty)", modifier = Modifier.fillMaxWidth().padding(10.dp), textAlign = TextAlign.Center) + Text( + text = "(empty)", modifier = Modifier + .fillMaxWidth() + .padding(10.dp), textAlign = TextAlign.Center + ) } } @@ -149,31 +169,41 @@ class SocialSection : Section() { ) }, ) { - when (scope) { - SocialScope.GROUP -> { - val group = groupList[index] - Column( - modifier = Modifier - .padding(10.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.Center - ) { - Text(text = group.name, maxLines = 1, fontWeight = FontWeight.Bold) + Row( + modifier = Modifier + .padding(10.dp) + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically + ) { + when (scope) { + SocialScope.GROUP -> { + val group = groupList[index] + Column( + modifier = Modifier + .padding(10.dp) + .fillMaxWidth() + .weight(1f) + ) { + Text( + text = group.name, + maxLines = 1, + fontWeight = FontWeight.Bold + ) + } } - } - SocialScope.FRIEND -> { - val friend = friendList[index] - val streaks = remember { context.modDatabase.getFriendStreaks(friend.userId) } - Row( - modifier = Modifier - .padding(10.dp) - .fillMaxSize(), - verticalAlignment = Alignment.CenterVertically - ) { + SocialScope.FRIEND -> { + val friend = friendList[index] + val streaks = + remember { context.modDatabase.getFriendStreaks(friend.userId) } + BitmojiImage( context = context, - url = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D) + url = BitmojiSelfie.getBitmojiSelfie( + friend.selfieId, + friend.bitmojiId, + BitmojiSelfie.BitmojiSelfieType.THREE_D + ) ) Column( modifier = Modifier @@ -181,8 +211,17 @@ class SocialSection : Section() { .fillMaxWidth() .weight(1f) ) { - Text(text = friend.displayName ?: friend.mutableUsername, maxLines = 1, fontWeight = FontWeight.Bold) - Text(text = friend.mutableUsername, maxLines = 1, fontSize = 12.sp, fontWeight = FontWeight.Light) + Text( + text = friend.displayName ?: friend.mutableUsername, + maxLines = 1, + fontWeight = FontWeight.Bold + ) + Text( + text = friend.mutableUsername, + maxLines = 1, + fontSize = 12.sp, + fontWeight = FontWeight.Light + ) } Row( verticalAlignment = Alignment.CenterVertically @@ -197,7 +236,11 @@ class SocialSection : Section() { else MaterialTheme.colorScheme.primary ) Text( - text = context.translation.format("manager.sections.social.streaks_expiration_short", "hours" to ((streaks.expirationTimestamp - System.currentTimeMillis()) / 3600000).toInt().toString()), + text = context.translation.format( + "manager.sections.social.streaks_expiration_short", + "hours" to ((streaks.expirationTimestamp - System.currentTimeMillis()) / 3600000).toInt() + .toString() + ), maxLines = 1, fontWeight = FontWeight.Bold ) @@ -205,6 +248,17 @@ class SocialSection : Section() { } } } + + FilledIconButton(onClick = { + navController.navigate( + MESSAGING_PREVIEW_ROUTE.replace("{id}", id).replace("{scope}", scope.key) + ) + }) { + Icon( + imageVector = Icons.Filled.RemoveRedEye, + contentDescription = null + ) + } } } } @@ -258,15 +312,24 @@ class SocialSection : Section() { selected = pagerState.currentPage == index, onClick = { coroutineScope.launch { - pagerState.animateScrollToPage( index ) + pagerState.animateScrollToPage(index) } }, - text = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) } + text = { + Text( + text = title, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } ) } } - HorizontalPager(modifier = Modifier.padding(paddingValues), state = pagerState) { page -> + HorizontalPager( + modifier = Modifier.padding(paddingValues), + state = pagerState + ) { page -> when (page) { 0 -> ScopeList(SocialScope.FRIEND) 1 -> ScopeList(SocialScope.GROUP) diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -7,6 +7,7 @@ import me.rhunk.snapenhance.bridge.scripting.IScripting; import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface; import me.rhunk.snapenhance.bridge.MessageLoggerInterface; import me.rhunk.snapenhance.bridge.ConfigStateListener; +import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge; interface BridgeInterface { /** @@ -80,6 +81,8 @@ interface BridgeInterface { MessageLoggerInterface getMessageLogger(); + void registerMessagingBridge(MessagingBridge bridge); + void openSettingsOverlay(); void closeSettingsOverlay(); diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/snapclient/MessagingBridge.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/snapclient/MessagingBridge.aidl @@ -0,0 +1,16 @@ +package me.rhunk.snapenhance.bridge.snapclient; + +import java.util.List; +import me.rhunk.snapenhance.bridge.snapclient.types.Message; + +interface MessagingBridge { + @nullable Message fetchMessage(String conversationId, String clientMessageId); + + @nullable Message fetchMessageByServerId(String conversationId, String serverMessageId); + + @nullable List<Message> fetchConversationWithMessagesPaginated(String conversationId, int limit, long beforeMessageId); + + @nullable String updateMessage(String conversationId, String clientMessageId, String messageUpdate); + + @nullable String getOneToOneConversationId(String userId); +}+ \ No newline at end of file diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/snapclient/types/Message.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/snapclient/types/Message.aidl @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.bridge.snapclient.types; + +parcelable Message { + String conversationId; + String senderId; + int contentType; + long clientMessageId; + long serverMessageId; + byte[] content; + List<String> mediaReferences; +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -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() 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 @@ -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>) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -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() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt @@ -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() 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 @@ -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 +}+ \ No newline at end of file