feat(core/ui): conversation toolbox

- add scripting support
This commit is contained in:
rhunk
2023-12-27 17:39:46 +01:00
parent 90d76c6412
commit 2a8fcacd2f
5 changed files with 216 additions and 54 deletions

View File

@ -62,7 +62,7 @@ class E2EEImplementation (
override fun acceptPairingRequest(friendId: String, publicKey: ByteArray): ByteArray? { override fun acceptPairingRequest(friendId: String, publicKey: ByteArray): ByteArray? {
val kemGen = KyberKEMGenerator(secureRandom) val kemGen = KyberKEMGenerator(secureRandom)
val encapsulatedSecret = runCatching { val encapsulatedSecret = runCatching {
kemGen.generateEncapsulated( kemGen.generateEncapsulated(
KyberPublicKeyParameters( KyberPublicKeyParameters(
kyberDefaultParameters, kyberDefaultParameters,
@ -164,7 +164,7 @@ class E2EEImplementation (
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKey, "AES"), IvParameterSpec(iv)) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKey, "AES"), IvParameterSpec(iv))
cipher.doFinal(message) cipher.doFinal(message)
}.onFailure { }.onFailure {
context.log.error("Failed to decrypt message from $friendId", it) context.log.warn("Failed to decrypt message for $friendId")
return null return null
}.getOrNull() }.getOrNull()
} }

View File

@ -8,4 +8,5 @@ enum class EnumScriptInterface(
) { ) {
SETTINGS("settings", BindingSide.MANAGER), SETTINGS("settings", BindingSide.MANAGER),
FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE), FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE),
CONVERSATION_TOOLBOX("conversationToolbox", BindingSide.CORE),
} }

View File

@ -7,10 +7,14 @@ import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.Shape import android.graphics.drawable.shapes.Shape
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Button import android.widget.Button
import android.widget.TextView import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import me.rhunk.snapenhance.common.data.ContentType import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.MessageState import me.rhunk.snapenhance.common.data.MessageState
import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.common.data.MessagingRuleType
@ -18,14 +22,13 @@ import me.rhunk.snapenhance.common.data.RuleState
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.MessagingRuleFeature import me.rhunk.snapenhance.core.features.MessagingRuleFeature
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.ui.ConversationToolbox
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.ui.addForegroundDrawable import me.rhunk.snapenhance.core.ui.addForegroundDrawable
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
@ -157,56 +160,34 @@ class EndToEndEncryption : MessagingRuleFeature(
} }
} }
private fun openManagementPopup() {
val conversationId = context.feature(Messaging::class).openedConversationUUID?.toString() ?: return
val friendId = context.database.getDMOtherParticipant(conversationId)
if (friendId == null) {
context.shortToast("This menu is only available in direct messages.")
return
}
val actions = listOf(
"Initiate a new shared secret",
"Show shared key fingerprint"
)
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!).apply {
setTitle("End-to-end encryption")
setItems(actions.toTypedArray()) { _, which ->
when (which) {
0 -> {
warnKeyOverwrite(friendId) {
askForKeys(conversationId)
}
}
1 -> {
val fingerprint = e2eeInterface.getSecretFingerprint(friendId)
ViewAppearanceHelper.newAlertDialogBuilder(context).apply {
setTitle("End-to-end encryption")
setMessage("Your fingerprint is:\n\n$fingerprint\n\nMake sure to check if it matches your friend's fingerprint!")
setPositiveButton("OK") { _, _ -> }
}.show()
}
}
}
setPositiveButton("OK") { _, _ -> }
}.show()
}
@SuppressLint("SetTextI18n", "DiscouragedApi") @SuppressLint("SetTextI18n", "DiscouragedApi")
override fun onActivityCreate() { override fun onActivityCreate() {
if (!isEnabled) return if (!isEnabled) return
// add button to input bar
context.event.subscribe(AddViewEvent::class) { param -> context.feature(ConversationToolbox::class).addComposable("End-to-end Encryption", filter = {
if (param.view.toString().contains("default_input_bar")) { context.database.getDMOtherParticipant(it) != null
(param.view as ViewGroup).addView(TextView(param.view.context).apply { }) { dialog, conversationId ->
layoutParams = MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT) val friendId = remember {
setOnClickListener { openManagementPopup() } context.database.getDMOtherParticipant(conversationId)
setPadding(20, 20, 20, 20) } ?: return@addComposable
textSize = 23f val fingerprint = remember {
text = "\uD83D\uDD12" runCatching {
}) e2eeInterface.getSecretFingerprint(friendId)
}.getOrNull()
}
if (fingerprint != null) {
Text("Your fingerprint is:\n\n$fingerprint\n\nMake sure to check if it matches your friend's fingerprint!")
} else {
Text("You don't have a shared secret with this friend yet. Click below to initiate a new one.")
}
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = {
dialog.dismiss()
warnKeyOverwrite(friendId) {
askForKeys(conversationId)
}
}) {
Text("Initiate new shared secret")
} }
} }
@ -245,6 +226,10 @@ class EndToEndEncryption : MessagingRuleFeature(
viewGroup.addView(Button(context.mainActivity!!).apply { viewGroup.addView(Button(context.mainActivity!!).apply {
text = "Accept secret" text = "Accept secret"
tag = receiveSecretTag tag = receiveSecretTag
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
setOnClickListener { setOnClickListener {
handleSecretResponse(conversationId, secret) handleSecretResponse(conversationId, secret)
} }
@ -255,6 +240,10 @@ class EndToEndEncryption : MessagingRuleFeature(
viewGroup.addView(Button(context.mainActivity!!).apply { viewGroup.addView(Button(context.mainActivity!!).apply {
text = "Receive public key" text = "Receive public key"
tag = receivePublicKeyTag tag = receivePublicKeyTag
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
setOnClickListener { setOnClickListener {
handlePublicKeyRequest(conversationId, publicKey) handlePublicKeyRequest(conversationId, publicKey)
} }

View File

@ -0,0 +1,171 @@
package me.rhunk.snapenhance.core.features.impl.ui
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface
import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.util.ktx.getId
data class ComposableMenu(
val title: String,
val filter: (conversationId: String) -> Boolean,
val composable: @Composable (alertDialog: AlertDialog, conversationId: String) -> Unit,
)
class ConversationToolbox : Feature("Conversation Toolbox", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
private val composableList = mutableListOf<ComposableMenu>()
private val expandedComposableCache = mutableStateMapOf<String, Boolean>()
fun addComposable(title: String, filter: (conversationId: String) -> Boolean = { true }, composable: @Composable (alertDialog: AlertDialog, conversationId: String) -> Unit) {
composableList.add(
ComposableMenu(title, filter, composable)
)
}
@SuppressLint("SetTextI18n")
override fun onActivityCreate() {
val defaultInputBarId = context.resources.getId("default_input_bar")
context.event.subscribe(AddViewEvent::class) { event ->
if (event.view.id != defaultInputBarId) return@subscribe
if (composableList.isEmpty()) return@subscribe
(event.view as ViewGroup).addView(FrameLayout(event.view.context).apply {
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
(52 * context.resources.displayMetrics.density).toInt(),
).apply {
gravity = Gravity.BOTTOM
}
setPadding(25, 0, 25, 0)
addView(TextView(event.view.context).apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).apply {
gravity = Gravity.CENTER_VERTICAL
}
setOnClickListener {
openToolbox()
}
textSize = 21f
text = "\uD83E\uDDF0"
})
})
}
context.scriptRuntime.eachModule {
val interfaceManager = getBinding(InterfaceManager::class)?.takeIf {
it.hasInterface(EnumScriptInterface.CONVERSATION_TOOLBOX)
} ?: return@eachModule
addComposable("\uD83D\uDCDC ${moduleInfo.displayName}") { alertDialog, conversationId ->
ScriptInterface(remember {
interfaceManager.buildInterface(EnumScriptInterface.CONVERSATION_TOOLBOX, mapOf(
"alertDialog" to alertDialog,
"conversationId" to conversationId,
))
} ?: return@addComposable)
}
}
}
private fun openToolbox() {
val openedConversationId = context.feature(Messaging::class).openedConversationUUID?.toString() ?: run {
context.shortToast("You must open a conversation first")
return
}
createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(
min = 100.dp,
max = LocalConfiguration.current.screenHeightDp * 0.8f.dp
)
.verticalScroll(rememberScrollState())
) {
Text("Conversation Toolbox", fontSize = 20.sp, modifier = Modifier
.fillMaxWidth()
.padding(10.dp), textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(10.dp))
composableList.reversed().forEach { (title, filter, composable) ->
if (!filter(openedConversationId)) return@forEach
Card(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp),
shape = MaterialTheme.shapes.medium
) {
Row(
modifier = Modifier
.clickable {
expandedComposableCache[title] = !(expandedComposableCache[title] ?: false)
}
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = if (expandedComposableCache[title] == true) Icons.Filled.KeyboardArrowDown else Icons.Filled.KeyboardArrowUp,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
)
Text(title, fontSize = 16.sp, fontStyle = FontStyle.Italic)
}
if (expandedComposableCache[title] == true) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
runCatching {
composable(alertDialog, openedConversationId)
}.onFailure { throwable ->
Text("Failed to load composable: ${throwable.message}")
context.log.error("Failed to load composable: ${throwable.message}", throwable)
}
}
}
}
}
}
}.show()
}
}

View File

@ -118,6 +118,7 @@ class FeatureManager(
EditTextOverride::class, EditTextOverride::class,
PreventForcedLogout::class, PreventForcedLogout::class,
SuspendLocationUpdates::class, SuspendLocationUpdates::class,
ConversationToolbox::class,
) )
initializeFeatures() initializeFeatures()