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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
}