feat: screener for message requests #24

Merged
reya merged 6 commits from feat/screening into master 2026-06-19 09:06:50 +00:00
3 changed files with 72 additions and 3 deletions
Showing only changes of commit 31a20122d3 - Show all commits

View File

@@ -27,16 +27,19 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_chat
@@ -44,6 +47,7 @@ import coop.composeapp.generated.resources.ic_share
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import rust.nostr.sdk.Timestamp
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@@ -64,9 +68,9 @@ fun ProfileScreen(pubkey: String, screening: Boolean = false) {
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
val metadataFlow = remember(pubkey) { viewModel.getMetadata(pubkey) }
val metadata by metadataFlow.collectAsState(initial = null)
val profile = metadata?.asRecord()
val metadata by metadataFlow.collectAsStateWithLifecycle()
val profile = metadata?.asRecord()
val displayName = profile?.displayName ?: profile?.name ?: "No name"
val nip05 = profile?.nip05 ?: pubkey.short()
val picture = profile?.picture
@@ -79,6 +83,22 @@ fun ProfileScreen(pubkey: String, screening: Boolean = false) {
)
}
var addressVerified by remember { mutableStateOf(false) }
var lastActivity by remember { mutableStateOf<Timestamp?>(null) }
LaunchedEffect(screening) {
if (screening) {
scope.launch {
// Verify NIP-05 address if present
profile?.nip05?.let { it ->
viewModel.verifyAddress(pubkey, it).let { addressVerified = it }
}
// Get the last activity
viewModel.verifyActivity(pubkey)?.let { lastActivity = it }
}
}
}
Scaffold(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
snackbarHost = { SnackbarHost(snackbarHostState) },

View File

@@ -947,4 +947,34 @@ class Nostr {
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
}
}
suspend fun verifyAddress(pubkey: PublicKey, address: String): Boolean {
try {
val address = Nip05Address.parse(address)
val profile = profileFromAddress(HttpClient(), address)
return profile.publicKey() == pubkey
} catch (e: Exception) {
throw IllegalStateException("Failed to verify address: ${e.message}", e)
}
}
suspend fun verifyActivity(pubkey: PublicKey): Timestamp? {
try {
val filter = Filter().author(pubkey).limit(3u)
val target = mutableMapOf<RelayUrl, List<Filter>>()
NostrManager.BOOTSTRAP_RELAYS.forEach { relay ->
target[RelayUrl.parse(relay)] = listOf(filter)
}
val events = client?.fetchEvents(
target = ReqTarget.manual(target),
timeout = Duration.parse("3s")
)
return events?.first()?.createdAt()
} catch (e: Exception) {
throw IllegalStateException("Failed to get latest activity: ${e.message}", e)
}
}
}

View File

@@ -35,6 +35,7 @@ import rust.nostr.sdk.PublicKey
import rust.nostr.sdk.RelayMetadata
import rust.nostr.sdk.RelayUrl
import rust.nostr.sdk.Tag
import rust.nostr.sdk.Timestamp
import rust.nostr.sdk.UnsignedEvent
import su.reya.coop.blossom.BlossomClient
import su.reya.coop.storage.SecretStorage
@@ -820,6 +821,24 @@ class NostrViewModel(
}
return emptyList()
}
suspend fun verifyAddress(pubkey: PublicKey, address: String): Boolean {
return try {
nostr.verifyAddress(pubkey, address)
} catch (e: Exception) {
showError("Error: ${e.message}")
false
}
}
suspend fun verifyActivity(pubkey: PublicKey): Timestamp? {
return try {
nostr.verifyActivity(pubkey)
} catch (e: Exception) {
showError("Error: ${e.message}")
null
}
}
}
fun PublicKey.short(): String {