add extract rumor
This commit is contained in:
@@ -4,7 +4,9 @@ import rust.nostr.sdk.Client
|
|||||||
import rust.nostr.sdk.ClientBuilder
|
import rust.nostr.sdk.ClientBuilder
|
||||||
import rust.nostr.sdk.ClientNotification
|
import rust.nostr.sdk.ClientNotification
|
||||||
import rust.nostr.sdk.Contact
|
import rust.nostr.sdk.Contact
|
||||||
|
import rust.nostr.sdk.Event
|
||||||
import rust.nostr.sdk.EventBuilder
|
import rust.nostr.sdk.EventBuilder
|
||||||
|
import rust.nostr.sdk.EventId
|
||||||
import rust.nostr.sdk.Filter
|
import rust.nostr.sdk.Filter
|
||||||
import rust.nostr.sdk.GossipConfig
|
import rust.nostr.sdk.GossipConfig
|
||||||
import rust.nostr.sdk.Keys
|
import rust.nostr.sdk.Keys
|
||||||
@@ -18,18 +20,24 @@ import rust.nostr.sdk.NostrGossip
|
|||||||
import rust.nostr.sdk.NostrSigner
|
import rust.nostr.sdk.NostrSigner
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import rust.nostr.sdk.RelayCapabilities
|
import rust.nostr.sdk.RelayCapabilities
|
||||||
|
import rust.nostr.sdk.RelayMessageEnum
|
||||||
import rust.nostr.sdk.RelayMetadata
|
import rust.nostr.sdk.RelayMetadata
|
||||||
import rust.nostr.sdk.RelayUrl
|
import rust.nostr.sdk.RelayUrl
|
||||||
import rust.nostr.sdk.ReqExitPolicy
|
import rust.nostr.sdk.ReqExitPolicy
|
||||||
import rust.nostr.sdk.ReqTarget
|
import rust.nostr.sdk.ReqTarget
|
||||||
import rust.nostr.sdk.SubscribeAutoCloseOptions
|
import rust.nostr.sdk.SubscribeAutoCloseOptions
|
||||||
|
import rust.nostr.sdk.Tag
|
||||||
import rust.nostr.sdk.Timestamp
|
import rust.nostr.sdk.Timestamp
|
||||||
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
|
import rust.nostr.sdk.UnwrappedGift
|
||||||
|
|
||||||
class Nostr {
|
class Nostr {
|
||||||
var client: Client? = null
|
var client: Client? = null
|
||||||
private set
|
private set
|
||||||
var signer: NostrSigner? = null
|
var signer: NostrSigner? = null
|
||||||
private set
|
private set
|
||||||
|
var deviceSigner: NostrSigner? = null
|
||||||
|
private set
|
||||||
|
|
||||||
suspend fun init(dbPath: String) {
|
suspend fun init(dbPath: String) {
|
||||||
val lmdb = NostrDatabase.lmdb(dbPath)
|
val lmdb = NostrDatabase.lmdb(dbPath)
|
||||||
@@ -83,21 +91,26 @@ class Nostr {
|
|||||||
suspend fun getUserMetadata() {
|
suspend fun getUserMetadata() {
|
||||||
val userPubkey = signer?.getPublicKey() ?: return
|
val userPubkey = signer?.getPublicKey() ?: return
|
||||||
|
|
||||||
val filter = Filter().author(userPubkey).limit(10u).kinds(
|
// Get the latest metadata event
|
||||||
listOf(
|
val metadataFilter =
|
||||||
Kind.fromStd(KindStandard.METADATA),
|
Filter().author(userPubkey).limit(1u).kind(Kind.fromStd(KindStandard.METADATA))
|
||||||
Kind.fromStd(KindStandard.CONTACT_LIST),
|
|
||||||
Kind.fromStd(KindStandard.INBOX_RELAYS)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val target = ReqTarget.auto(listOf(filter))
|
// Get the latest contact list event
|
||||||
|
val contactFilter =
|
||||||
|
Filter().author(userPubkey).limit(1u).kind(Kind.fromStd(KindStandard.CONTACT_LIST))
|
||||||
|
|
||||||
|
// Get the latest messaging relay list event
|
||||||
|
val msgRelayFilter =
|
||||||
|
Filter().author(userPubkey).limit(1u).kind(Kind.fromStd(KindStandard.INBOX_RELAYS))
|
||||||
|
|
||||||
|
// Construct a target that includes all filters
|
||||||
|
val target = ReqTarget.auto(listOf(metadataFilter, contactFilter, msgRelayFilter))
|
||||||
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
||||||
|
|
||||||
client?.subscribe(target = target, id = "user-metadata", closeOn = opts)
|
client?.subscribe(target = target, id = "user-metadata", closeOn = opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleNotifications() {
|
suspend fun handleNotifications(onMetadataUpdate: (PublicKey, Metadata) -> Unit) {
|
||||||
val now = Timestamp.now()
|
val now = Timestamp.now()
|
||||||
val notifications = client?.notifications()
|
val notifications = client?.notifications()
|
||||||
|
|
||||||
@@ -106,7 +119,42 @@ class Nostr {
|
|||||||
|
|
||||||
when (notification) {
|
when (notification) {
|
||||||
is ClientNotification.Message -> {
|
is ClientNotification.Message -> {
|
||||||
// TODO: Handle message
|
val relayUrl = notification.relayUrl
|
||||||
|
val message = notification.message.asEnum()
|
||||||
|
|
||||||
|
when (message) {
|
||||||
|
is RelayMessageEnum.EventMsg -> {
|
||||||
|
val event = message.event
|
||||||
|
|
||||||
|
|
||||||
|
if (event.kind().asStd() == KindStandard.METADATA) {
|
||||||
|
try {
|
||||||
|
val metadata = Metadata.fromJson(event.content())
|
||||||
|
onMetadataUpdate(event.author(), metadata)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Failed to parse metadata: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.kind().asStd() == KindStandard.GIFT_WRAP) {
|
||||||
|
try {
|
||||||
|
val rumor = extractRumor(event)
|
||||||
|
// TODO: Handle rumor
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Failed to extract rumor: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is RelayMessageEnum.EndOfStoredEvents -> {
|
||||||
|
val subscriptionId = message.subscriptionId
|
||||||
|
// TODO: Handle end of stored events
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
/* Ignore other message types */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ClientNotification.NewEvent -> {
|
is ClientNotification.NewEvent -> {
|
||||||
@@ -120,6 +168,73 @@ class Nostr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getCachedRumor(giftId: EventId): UnsignedEvent? {
|
||||||
|
try {
|
||||||
|
val filter = Filter().identifier(giftId.toBech32())
|
||||||
|
val event = client?.database()?.query(filter)?.first()
|
||||||
|
|
||||||
|
return event?.content()?.let { UnsignedEvent.fromJson(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// TODO: log error
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setCachedRumor(giftId: EventId, rumor: UnsignedEvent) {
|
||||||
|
if (rumor.id() == null) return
|
||||||
|
try {
|
||||||
|
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA);
|
||||||
|
val tags = listOf(Tag.identifier(giftId.toBech32()), Tag.event(rumor.id()!!))
|
||||||
|
val event = EventBuilder(kind, rumor.asJson()).tags(tags).signWithKeys(Keys.generate())
|
||||||
|
|
||||||
|
client?.database()?.saveEvent(event)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// TODO: log error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun extractRumor(event: Event): UnsignedEvent? {
|
||||||
|
if (event.kind().asStd() != KindStandard.GIFT_WRAP) return null
|
||||||
|
|
||||||
|
// Check if the rumor is already cached
|
||||||
|
val cachedRumor = getCachedRumor(event.id())
|
||||||
|
if (cachedRumor != null) return cachedRumor
|
||||||
|
|
||||||
|
// Get all signers
|
||||||
|
val signers = listOfNotNull(signer, deviceSigner)
|
||||||
|
if (signers.isEmpty()) return null
|
||||||
|
|
||||||
|
// Try to unwrap the gift with each signer
|
||||||
|
for (signer in signers) {
|
||||||
|
try {
|
||||||
|
// TODO: custom unwrapping logic
|
||||||
|
val gift = UnwrappedGift.fromGiftWrap(signer = signer, giftWrap = event)
|
||||||
|
val rumor = gift.rumor()
|
||||||
|
// Save the rumor to the database
|
||||||
|
setCachedRumor(event.id(), rumor)
|
||||||
|
// Return the rumor
|
||||||
|
return rumor
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// TODO: log error
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun conversationId(rumor: UnsignedEvent): Long {
|
||||||
|
val pubkeys: MutableList<PublicKey> = rumor.tags().publicKeys().toMutableList()
|
||||||
|
pubkeys.add(rumor.author())
|
||||||
|
|
||||||
|
val uniqueSortedKeys = pubkeys
|
||||||
|
.map { it.toHex() }
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
return uniqueSortedKeys.hashCode().toLong()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getDefaultRelayList(): Map<RelayUrl, RelayMetadata> {
|
suspend fun getDefaultRelayList(): Map<RelayUrl, RelayMetadata> {
|
||||||
// Construct a list of relays
|
// Construct a list of relays
|
||||||
val relayList = mapOf<RelayUrl, RelayMetadata>(
|
val relayList = mapOf<RelayUrl, RelayMetadata>(
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import rust.nostr.sdk.Keys
|
import rust.nostr.sdk.Keys
|
||||||
|
import rust.nostr.sdk.Metadata
|
||||||
import rust.nostr.sdk.NostrConnect
|
import rust.nostr.sdk.NostrConnect
|
||||||
import rust.nostr.sdk.NostrConnectUri
|
import rust.nostr.sdk.NostrConnectUri
|
||||||
|
import rust.nostr.sdk.PublicKey
|
||||||
import su.reya.coop.storage.SecretStorage
|
import su.reya.coop.storage.SecretStorage
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@@ -23,6 +26,17 @@ class NostrViewModel(
|
|||||||
private val _isCreating = MutableStateFlow(false)
|
private val _isCreating = MutableStateFlow(false)
|
||||||
val isCreating = _isCreating.asStateFlow()
|
val isCreating = _isCreating.asStateFlow()
|
||||||
|
|
||||||
|
// User metadata store
|
||||||
|
private val _metadataStore = mutableMapOf<PublicKey, MutableStateFlow<Metadata?>>()
|
||||||
|
|
||||||
|
fun getMetadata(pubkey: PublicKey): StateFlow<Metadata?> {
|
||||||
|
return _metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMetadata(pubkey: PublicKey, metadata: Metadata) {
|
||||||
|
_metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }.value = metadata
|
||||||
|
}
|
||||||
|
|
||||||
fun initAndConnect(dbPath: String) {
|
fun initAndConnect(dbPath: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
@@ -67,7 +81,9 @@ class NostrViewModel(
|
|||||||
|
|
||||||
fun startNotificationHandler() {
|
fun startNotificationHandler() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
nostr.handleNotifications()
|
nostr.handleNotifications { pubkey, metadata ->
|
||||||
|
updateMetadata(pubkey, metadata)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user