fix
This commit is contained in:
75
shared/src/commonMain/kotlin/su/reya/coop/CoopWebSocket.kt
Normal file
75
shared/src/commonMain/kotlin/su/reya/coop/CoopWebSocket.kt
Normal file
@@ -0,0 +1,75 @@
|
||||
package su.reya.coop
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readBytes
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import rust.nostr.sdk.ConnectionMode
|
||||
import rust.nostr.sdk.CustomWebSocketTransport
|
||||
import rust.nostr.sdk.WebSocketAdapter
|
||||
import rust.nostr.sdk.WebSocketAdapterWrapper
|
||||
import rust.nostr.sdk.WebSocketMessage
|
||||
|
||||
class KtorWebSocketAdapter(
|
||||
private val client: HttpClient,
|
||||
private val session: DefaultClientWebSocketSession
|
||||
) : WebSocketAdapter {
|
||||
|
||||
override suspend fun send(msg: WebSocketMessage) {
|
||||
try {
|
||||
when (msg) {
|
||||
is WebSocketMessage.Text -> session.send(Frame.Text(msg.text))
|
||||
is WebSocketMessage.Binary -> session.send(Frame.Binary(true, msg.bytes))
|
||||
is WebSocketMessage.Ping -> session.send(Frame.Ping(msg.bytes))
|
||||
is WebSocketMessage.Pong -> session.send(Frame.Pong(msg.bytes))
|
||||
else -> {}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Attempted to send on a closed WebSocket: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun recv(): WebSocketMessage? {
|
||||
return try {
|
||||
when (val frame = session.incoming.receive()) {
|
||||
is Frame.Text -> WebSocketMessage.Text(frame.readText())
|
||||
is Frame.Binary -> WebSocketMessage.Binary(frame.readBytes())
|
||||
is Frame.Ping -> WebSocketMessage.Ping(frame.readBytes())
|
||||
is Frame.Pong -> WebSocketMessage.Pong(frame.readBytes())
|
||||
else -> null
|
||||
}
|
||||
} catch (e: ClosedReceiveChannelException) {
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun closeConnection() {
|
||||
session.cancel()
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
class CoopWebSocketClient(private val httpClient: HttpClient) : CustomWebSocketTransport {
|
||||
override fun supportPing(): Boolean = false
|
||||
|
||||
override suspend fun connect(url: String, mode: ConnectionMode): WebSocketAdapterWrapper {
|
||||
try {
|
||||
val session = httpClient.webSocketSession {
|
||||
url(url)
|
||||
}
|
||||
val adapter = KtorWebSocketAdapter(httpClient, session)
|
||||
return WebSocketAdapterWrapper(adapter)
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package su.reya.coop
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import rust.nostr.sdk.Client
|
||||
import rust.nostr.sdk.ClientBuilder
|
||||
import rust.nostr.sdk.ClientNotification
|
||||
@@ -12,6 +14,7 @@ import rust.nostr.sdk.GossipConfig
|
||||
import rust.nostr.sdk.Keys
|
||||
import rust.nostr.sdk.Kind
|
||||
import rust.nostr.sdk.KindStandard
|
||||
import rust.nostr.sdk.LogLevel
|
||||
import rust.nostr.sdk.Metadata
|
||||
import rust.nostr.sdk.MetadataRecord
|
||||
import rust.nostr.sdk.NostrConnect
|
||||
@@ -32,6 +35,7 @@ import rust.nostr.sdk.Timestamp
|
||||
import rust.nostr.sdk.UnsignedEvent
|
||||
import rust.nostr.sdk.UnwrappedGift
|
||||
import rust.nostr.sdk.extractMessagingRelayList
|
||||
import rust.nostr.sdk.initLogger
|
||||
import kotlin.time.Duration
|
||||
|
||||
class Nostr {
|
||||
@@ -47,39 +51,37 @@ class Nostr {
|
||||
private set
|
||||
|
||||
suspend fun init(dbPath: String) {
|
||||
val lmdb = NostrDatabase.lmdb(dbPath)
|
||||
val gossip = NostrGossip.inMemory()
|
||||
val idleTimeout = Duration.parse("5m")
|
||||
|
||||
client =
|
||||
ClientBuilder()
|
||||
.database(lmdb)
|
||||
.gossip(gossip)
|
||||
.gossipConfig(GossipConfig().noBackgroundRefresh())
|
||||
.maxRelays(20u)
|
||||
.verifySubscriptions(false)
|
||||
.automaticAuthentication(false)
|
||||
.sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun connect() {
|
||||
try {
|
||||
client?.addRelay(
|
||||
url = RelayUrl.parse("wss://relay.primal.net"),
|
||||
capabilities = RelayCapabilities.none()
|
||||
)
|
||||
client?.addRelay(
|
||||
url = RelayUrl.parse("wss://user.kindpag.es"),
|
||||
capabilities = RelayCapabilities.none()
|
||||
)
|
||||
// Initialize the logger for nostr client
|
||||
initLogger(LogLevel.DEBUG)
|
||||
|
||||
val lmdb = NostrDatabase.lmdb(dbPath)
|
||||
val gossip = NostrGossip.inMemory()
|
||||
val idleTimeout = Duration.parse("5m")
|
||||
val httpClient = HttpClient {
|
||||
install(WebSockets)
|
||||
}
|
||||
|
||||
client =
|
||||
ClientBuilder()
|
||||
.websocketTransport(CoopWebSocketClient(httpClient))
|
||||
.database(lmdb)
|
||||
.gossip(gossip)
|
||||
.gossipConfig(GossipConfig().noBackgroundRefresh())
|
||||
.verifySubscriptions(false)
|
||||
.automaticAuthentication(false)
|
||||
.sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
|
||||
.build()
|
||||
|
||||
client?.addRelay(RelayUrl.parse("wss://relay.primal.net"))
|
||||
client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
||||
client?.addRelay(
|
||||
url = RelayUrl.parse("wss://indexer.coracle.social"),
|
||||
capabilities = RelayCapabilities.gossip()
|
||||
)
|
||||
client?.connect()
|
||||
client?.connect(Duration.parse("10s"))
|
||||
} catch (e: Exception) {
|
||||
println("Failed to connect to relays: ${e.message}")
|
||||
println("Failed to initialize client: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +100,8 @@ class Nostr {
|
||||
try {
|
||||
signer = NostrSigner.keys(keys)
|
||||
userPubkey = signer?.getPublicKey()
|
||||
|
||||
// Fetch metadata for current user
|
||||
getUserMetadata()
|
||||
} catch (e: Exception) {
|
||||
println("Failed to set signer: ${e.message}")
|
||||
@@ -108,6 +112,8 @@ class Nostr {
|
||||
try {
|
||||
signer = NostrSigner.nostrConnect(remote)
|
||||
userPubkey = signer?.getPublicKey()
|
||||
|
||||
// Fetch metadata for current user
|
||||
getUserMetadata()
|
||||
} catch (e: Exception) {
|
||||
println("Failed to set remote signer: ${e.message}")
|
||||
@@ -123,19 +129,17 @@ class Nostr {
|
||||
}
|
||||
|
||||
suspend fun getUserMetadata() {
|
||||
val userPubkey = signer?.getPublicKey() ?: return
|
||||
|
||||
// Get the latest metadata event
|
||||
val metadataFilter =
|
||||
Filter().author(userPubkey).limit(1u).kind(Kind.fromStd(KindStandard.METADATA))
|
||||
Filter().author(userPubkey!!).limit(1u).kind(Kind.fromStd(KindStandard.METADATA))
|
||||
|
||||
// Get the latest contact list event
|
||||
val contactFilter =
|
||||
Filter().author(userPubkey).limit(1u).kind(Kind.fromStd(KindStandard.CONTACT_LIST))
|
||||
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))
|
||||
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))
|
||||
@@ -170,11 +174,11 @@ class Nostr {
|
||||
|
||||
suspend fun handleNotifications(onMetadataUpdate: (PublicKey, Metadata) -> Unit) {
|
||||
val now = Timestamp.now()
|
||||
val notifications = client?.notifications()
|
||||
val processedEvent = mutableSetOf<EventId>()
|
||||
|
||||
val notifications = client?.notifications() ?: return
|
||||
|
||||
while (true) {
|
||||
val notification = notifications?.next() ?: break
|
||||
val notification = notifications.next() ?: continue
|
||||
|
||||
when (notification) {
|
||||
is ClientNotification.Message -> {
|
||||
@@ -189,7 +193,7 @@ class Nostr {
|
||||
if (processedEvent.contains(event.id())) continue
|
||||
processedEvent.add(event.id())
|
||||
|
||||
if (event.kind().asStd() == KindStandard.METADATA) {
|
||||
if (event.kind().asStd()?.equals(KindStandard.METADATA) == true) {
|
||||
try {
|
||||
val metadata = Metadata.fromJson(event.content())
|
||||
onMetadataUpdate(event.author(), metadata)
|
||||
@@ -198,13 +202,13 @@ class Nostr {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind().asStd() == KindStandard.INBOX_RELAYS) {
|
||||
if (event.kind().asStd()?.equals(KindStandard.INBOX_RELAYS) == true) {
|
||||
if (isSignedByUser(event = event)) {
|
||||
getUserMessages(msgRelayList = event)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind().asStd() == KindStandard.GIFT_WRAP) {
|
||||
if (event.kind().asStd()?.equals(KindStandard.GIFT_WRAP) == true) {
|
||||
try {
|
||||
val rumor = extractRumor(event)
|
||||
// TODO: Handle rumor
|
||||
|
||||
@@ -101,21 +101,17 @@ class NostrViewModel(
|
||||
}
|
||||
|
||||
fun getUserProfile(): StateFlow<Metadata?> {
|
||||
return getMetadata(nostr.userPubkey!!)
|
||||
return nostr.userPubkey?.let { getMetadata(it) } ?: MutableStateFlow(null).asStateFlow()
|
||||
}
|
||||
|
||||
fun initAndConnect(dbPath: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// Initialize nostr client
|
||||
nostr.init(dbPath)
|
||||
// Connect to bootstrap relays
|
||||
nostr.connect()
|
||||
// Get user's secret
|
||||
getUserSecret()
|
||||
} catch (e: Exception) {
|
||||
showError("Failed to initialize Nostr: ${e.message}")
|
||||
}
|
||||
suspend fun initAndConnect(dbPath: String) {
|
||||
try {
|
||||
// Initialize nostr client
|
||||
nostr.init(dbPath)
|
||||
// Get user's secret
|
||||
getUserSecret()
|
||||
} catch (e: Exception) {
|
||||
showError("Failed to initialize Nostr: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user