mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-08 02:14:33 +02:00
feat(manager): conversation preview (wip)
- add messaging bridge - refactor export chat messages
This commit is contained in:
parent
2b0e4ad09a
commit
3e9c97c18c
@ -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.")
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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()
|
||||
|
@ -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>) {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user