Add Primal like NostrPublisher
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package ac.aux.compose.cryptography
|
||||
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import kotlin.time.Instant
|
||||
|
||||
fun UnsignedNostrEvent.signOrThrow(nsec: String): NostrEvent {
|
||||
val hexPrivateKey = Bech32.decodeBytes(nsec).second
|
||||
return this.signOrThrow(hexPrivateKey)
|
||||
}
|
||||
|
||||
fun UnsignedNostrEvent.signOrThrow(hexPrivateKey: ByteArray): NostrEvent {
|
||||
val tempSigner = NostrSignerSync(
|
||||
KeyPair(
|
||||
privKey = hexPrivateKey
|
||||
)
|
||||
)
|
||||
|
||||
val event = tempSigner.signNormal<Event>(
|
||||
createdAt = this.createdAt.epochSeconds,
|
||||
kind = this.kind,
|
||||
tags = this.tags,
|
||||
content = this.privateTags?.let { PrivateTagsInContent.encryptNip44(it, tempSigner) } ?: this.content
|
||||
)
|
||||
return NostrEvent(
|
||||
id = event.id,
|
||||
pubKey = event.pubKey,
|
||||
kind = event.kind,
|
||||
tags = event.tags,
|
||||
content = event.content,
|
||||
createdAt = Instant.fromEpochSeconds(event.createdAt),
|
||||
sig = event.sig,
|
||||
unsignedNostrEventId = this.id
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import ac.aux.compose.database.dao.PostDao
|
||||
import ac.aux.compose.database.dao.ProfileDao
|
||||
import ac.aux.compose.database.dao.ReactionDao
|
||||
import ac.aux.compose.database.dao.RecentSearchDao
|
||||
import ac.aux.compose.database.dao.RelayDao
|
||||
import ac.aux.compose.database.dao.RepostDao
|
||||
import ac.aux.compose.database.dao.SynchronizeNostrEventRequestDao
|
||||
import ac.aux.compose.database.dao.SynchronizeNostrEventResultDao
|
||||
@@ -66,6 +67,9 @@ abstract class AuxDatabase: RoomDatabase() {
|
||||
abstract fun reactionDao(): ReactionDao
|
||||
|
||||
abstract fun recentSearchDao(): RecentSearchDao
|
||||
|
||||
abstract fun relayDao(): RelayDao
|
||||
|
||||
abstract fun repostDao(): RepostDao
|
||||
|
||||
abstract fun synchronizeNostrEventRequestDao(): SynchronizeNostrEventRequestDao
|
||||
|
||||
@@ -13,7 +13,7 @@ interface RelayDao {
|
||||
fun getAllPosts(): List<Relay>
|
||||
|
||||
@Query("SELECT * FROM Relay WHERE publicKey = :publicKey")
|
||||
fun observePublicKeyRelays(publicKey: String): Flow<Relay?>
|
||||
fun observePublicKeyRelays(publicKey: String): Flow<List<Relay>>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(relay: Relay)
|
||||
|
||||
@@ -7,6 +7,7 @@ import ac.aux.compose.database.model.BroadcastNostrEventRequest
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.RecentSearch
|
||||
import ac.aux.compose.database.model.Relay
|
||||
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
|
||||
@@ -16,6 +17,7 @@ import ac.aux.compose.database.model.intermdiate.LocalProfile
|
||||
import ac.aux.compose.database.model.typealiases.SynchronizationFilterArray
|
||||
import ac.aux.compose.nostr.Relays
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.repository.RelayRepository
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
@@ -43,7 +45,7 @@ import kotlin.time.Instant
|
||||
|
||||
class DatabaseNostrRepository(
|
||||
private val database: AuxDatabase
|
||||
): NostrRepository {
|
||||
): NostrRepository, RelayRepository {
|
||||
companion object {
|
||||
const val TAG = "DatabaseNostrRepository"
|
||||
}
|
||||
@@ -350,4 +352,8 @@ class DatabaseNostrRepository(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun observePublicKeyRelays(publicKey: String): Flow<List<Relay>> {
|
||||
return database.relayDao().observePublicKeyRelays(publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package ac.aux.compose.exceptions
|
||||
|
||||
class InvalidNostrPrivateKeyException: RuntimeException()
|
||||
@@ -0,0 +1,3 @@
|
||||
package ac.aux.compose.exceptions
|
||||
|
||||
class NostrPublishException(override val cause: Throwable?) : RuntimeException()
|
||||
@@ -0,0 +1,4 @@
|
||||
package ac.aux.compose.exceptions
|
||||
|
||||
open class SignatureException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ac.aux.compose.exceptions
|
||||
|
||||
class SigningKeyNotFoundException(message: String? = null, cause: Throwable? = null) : SignatureException(
|
||||
message,
|
||||
cause,
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package ac.aux.compose.exceptions
|
||||
|
||||
class SigningRejectedException(message: String? = null, cause: Throwable? = null) : SignatureException(message, cause) {
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package ac.aux.compose.managers
|
||||
|
||||
import ac.aux.compose.exceptions.InvalidNostrPrivateKeyException
|
||||
import androidx.datastore.core.DataStore
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNsec
|
||||
import fr.acinq.bitcoin.PrivateKey
|
||||
import fr.acinq.lightning.Lightning
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import io.ktor.utils.io.core.toByteArray
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/***
|
||||
* TODO: Make this a Singleton
|
||||
*/
|
||||
class CredentialsManager(
|
||||
private val persistence: DataStore<Set<Credential>>,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
val credentials = persistence.data
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = runBlocking { persistence.data.first() },
|
||||
)
|
||||
|
||||
private suspend fun addCredential(credential: Credential) = persistence.updateData { it + credential }
|
||||
|
||||
suspend fun clearCredentials() = persistence.updateData { emptySet() }
|
||||
|
||||
fun isExternalSignerCredential(npub: String) =
|
||||
checkCredentialType(npub = npub, credentialType = CredentialType.ExternalSigner)
|
||||
|
||||
fun isNpubCredential(npub: String) = checkCredentialType(npub = npub, credentialType = CredentialType.PublicKey)
|
||||
|
||||
suspend fun getOrCreateInternalSignerCredentials() =
|
||||
credentials.value.find { it.type == CredentialType.InternalSigner }
|
||||
?: Lightning.randomKey().let { privateKey ->
|
||||
Credential(
|
||||
nsec = privateKey.value.toByteArray().toNsec(),
|
||||
npub = privateKey.publicKey().value.toByteArray().toNpub(),
|
||||
type = CredentialType.InternalSigner
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkCredentialType(npub: String, credentialType: CredentialType) =
|
||||
credentials.value.find { it.npub == npub }?.type == credentialType
|
||||
|
||||
suspend fun saveNsec(nostrKey: String): String {
|
||||
val (nsec, pubkey) = nostrKey.extractKeyPairFromPrivateKeyOrThrow()
|
||||
addCredential(Credential(nsec = nsec, npub = pubkey, type = CredentialType.PrivateKey))
|
||||
return pubkey.bech32ToHexOrThrow()
|
||||
}
|
||||
|
||||
suspend fun saveExternalSignerNpub(npub: String): String {
|
||||
val (hexKey, bech32Key) = if (npub.startsWith("npub")) {
|
||||
npub.bech32ToHexOrThrow() to npub
|
||||
} else {
|
||||
npub to npub.hexToNpubHrp()
|
||||
}
|
||||
|
||||
addCredential(Credential(nsec = null, npub = bech32Key, type = CredentialType.ExternalSigner))
|
||||
return hexKey
|
||||
}
|
||||
|
||||
suspend fun saveNpub(npub: String): String {
|
||||
addCredential(Credential(nsec = null, npub = npub, type = CredentialType.PublicKey))
|
||||
return npub.bech32ToHexOrThrow()
|
||||
}
|
||||
|
||||
suspend fun removeCredentialByNsec(nsec: String) =
|
||||
persistence.updateData {
|
||||
it.filterNot { cred -> cred.nsec == nsec }.toSet()
|
||||
}
|
||||
|
||||
suspend fun removeCredentialByNpub(npub: String) =
|
||||
persistence.updateData {
|
||||
it.filterNot { cred -> cred.npub == npub }.toSet()
|
||||
}
|
||||
|
||||
fun findOrThrow(npub: String): Credential =
|
||||
credentials.value.find { it.npub == npub }
|
||||
?: throw IllegalArgumentException("Credential not found for $npub.")
|
||||
|
||||
@Serializable
|
||||
data class Credential(
|
||||
val nsec: String?,
|
||||
val npub: String,
|
||||
val type: CredentialType = CredentialType.PrivateKey,
|
||||
)
|
||||
|
||||
enum class CredentialType {
|
||||
InternalSigner,
|
||||
ExternalSigner,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun String.assureValidNsec() = if (startsWith("nsec")) this else this.hexToNsecHrp()
|
||||
fun String.assureValidNpub() = if (startsWith("npub")) this else this.hexToNpubHrp()
|
||||
fun String.assureValidPubKeyHex() = if (startsWith("npub")) this.bech32ToHexOrThrow() else this
|
||||
|
||||
|
||||
fun String.hexToNoteHrp() =
|
||||
Bech32.encodeBytes(
|
||||
hrp = "note",
|
||||
data = Hex.decode(this),
|
||||
encoding = Bech32.Encoding.Bech32,
|
||||
)
|
||||
|
||||
fun String.hexToNpubHrp() =
|
||||
Bech32.encodeBytes(
|
||||
hrp = "npub",
|
||||
data = Hex.decode(this),
|
||||
encoding = Bech32.Encoding.Bech32,
|
||||
)
|
||||
|
||||
fun String.hexToNsecHrp() =
|
||||
Bech32.encodeBytes(
|
||||
hrp = "nsec",
|
||||
data = Hex.decode(this),
|
||||
encoding = Bech32.Encoding.Bech32,
|
||||
)
|
||||
|
||||
fun String.urlToLnUrlHrp() =
|
||||
Bech32.encodeBytes(
|
||||
hrp = "lnurl",
|
||||
data = this.toByteArray(),
|
||||
encoding = Bech32.Encoding.Bech32,
|
||||
)
|
||||
|
||||
|
||||
fun String.bech32ToHexOrThrow() = Bech32.decodeBytes(bech32 = this).second.toHex()
|
||||
|
||||
fun String.bech32ToHexOrNull() = runCatching { this.bech32ToHexOrThrow() }.getOrNull()
|
||||
|
||||
fun ByteArray.toNpub() = Bech32.encodeBytes(hrp = "npub", this, Bech32.Encoding.Bech32)
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun ByteArray.toHex() = Hex.encode(this)
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun String.bechToBytesOrThrow(hrp: String? = null): ByteArray {
|
||||
val decodedForm = Bech32.decodeBytes(this)
|
||||
hrp?.also { require(it == decodedForm.first) }
|
||||
return decodedForm.second
|
||||
}
|
||||
|
||||
fun String.extractKeyPairFromPrivateKeyOrThrow(): Pair<String, String> {
|
||||
return try {
|
||||
val nsec = this.assureValidNsec()
|
||||
val decoded = Bech32.decodeBytes(nsec)
|
||||
val pubkey = PrivateKey(decoded.second).publicKey().value.toByteArray()
|
||||
nsec to pubkey.toNpub()
|
||||
} catch (error: IllegalArgumentException) {
|
||||
Logger.withTag("String.extractKeyPairFromPrivateKeyOrThrow").w(error) { error.message ?: "" }
|
||||
throw InvalidNostrPrivateKeyException()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ac.aux.compose.network
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
|
||||
object UserAgent {
|
||||
const val APP_NAME = "Aux"
|
||||
const val CLIENT_NAME = "Aux"
|
||||
}
|
||||
|
||||
fun String.asClientTag(): Array<String> = arrayOf(
|
||||
"client",
|
||||
this
|
||||
)
|
||||
@@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.flow.timeout
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import net.primal.android.networking.relays.errors.NostrPublishException
|
||||
import ac.aux.compose.exceptions.NostrPublishException
|
||||
import ac.aux.compose.network.sockets.NostrIncomingMessage
|
||||
import ac.aux.compose.network.sockets.NostrSocketClient
|
||||
import ac.aux.compose.network.sockets.NostrSocketClientFactory
|
||||
@@ -30,6 +30,11 @@ import ac.aux.compose.network.sockets.parseIncomingMessage
|
||||
import ac.aux.compose.repository.CachingImportRepository
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
/**
|
||||
* As seen in Primal
|
||||
*
|
||||
* https://github.com/PrimalHQ/primal-android-app/blob/8a912053131764c39711cd0dc5012645498c9a4f/app/src/main/kotlin/net/primal/android/networking/relays/RelayPool.kt
|
||||
*/
|
||||
class RelayPool(
|
||||
private val nostrSocketClientFactory: NostrSocketClientFactory,
|
||||
private val cachingImportRepository: CachingImportRepository,
|
||||
|
||||
@@ -15,9 +15,14 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.primal.android.networking.relays.errors.NostrPublishException
|
||||
import ac.aux.compose.exceptions.NostrPublishException
|
||||
|
||||
|
||||
/**
|
||||
* As Seen in Primal
|
||||
*
|
||||
* https://github.com/PrimalHQ/primal-android-app/blob/6db2e6862239335dede4162338d7c4f10ad35031/app/src/main/kotlin/net/primal/android/networking/relays/RelaysSocketManager.kt
|
||||
*/
|
||||
class RelaysSocketManager constructor(
|
||||
private val nostrSocketClientFactory: NostrSocketClientFactory,
|
||||
private val cachingImportRepository: CachingImportRepository,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package net.primal.android.networking.relays.errors
|
||||
|
||||
class NostrPublishException(override val cause: Throwable?) : RuntimeException()
|
||||
@@ -3,10 +3,41 @@ package ac.aux.compose.repository
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.network.relays.broadcast.BroadcastEventResponse
|
||||
|
||||
/**
|
||||
* As seen in Primal
|
||||
*
|
||||
* https://github.com/PrimalHQ/primal-android-app/blob/a7e32e2203555d27e60a749238175cd1ee0fa2af/domain/primal/src/commonMain/kotlin/net/primal/domain/global/CachingImportRepository.kt
|
||||
*
|
||||
*/
|
||||
interface CachingImportRepository {
|
||||
suspend fun cacheNostrEvents(vararg events: NostrEvent)
|
||||
suspend fun cacheNostrEvents(events: List<NostrEvent>)
|
||||
|
||||
suspend fun importEvents(events: List<NostrEvent>): Boolean
|
||||
suspend fun broadcastEvents(events: List<NostrEvent>, relays: List<String>): Result<List<BroadcastEventResponse>>
|
||||
|
||||
companion object {
|
||||
val NO_OP_CACHING_IMPORT_REPOSITORY = object : CachingImportRepository {
|
||||
override suspend fun cacheNostrEvents(vararg events: NostrEvent) {
|
||||
}
|
||||
|
||||
override suspend fun cacheNostrEvents(events: List<NostrEvent>) {
|
||||
|
||||
}
|
||||
|
||||
override suspend fun importEvents(events: List<NostrEvent>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun broadcastEvents(
|
||||
events: List<NostrEvent>,
|
||||
relays: List<String>
|
||||
): Result<List<BroadcastEventResponse>> {
|
||||
return Result.success(
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package ac.aux.compose.repository
|
||||
|
||||
import ac.aux.compose.cryptography.signOrThrow
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.exceptions.SignatureException
|
||||
import ac.aux.compose.exceptions.SigningKeyNotFoundException
|
||||
import ac.aux.compose.exceptions.SigningRejectedException
|
||||
import ac.aux.compose.managers.CredentialsManager
|
||||
import ac.aux.compose.managers.hexToNpubHrp
|
||||
import ac.aux.compose.network.UserAgent
|
||||
import ac.aux.compose.network.asClientTag
|
||||
import ac.aux.compose.network.dto.RelayDTO
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* As seen in Primal
|
||||
*
|
||||
* https://github.com/PrimalHQ/primal-android-app/blob/86e8d29df7f780e8d19ccff2362d7dc6398da634/app/src/main/kotlin/net/primal/android/nostr/notary/NostrNotary.kt
|
||||
*
|
||||
* TODO: Make this a Singleton
|
||||
*/
|
||||
class NostrNotaryRepository(
|
||||
private val nostrRepository: NostrRepository,
|
||||
private val credentialsStore: CredentialsManager,
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
private val _effects = Channel<NotarySideEffect>()
|
||||
val effects = _effects.receiveAsFlow()
|
||||
private fun setEffect(effect: NotarySideEffect) = scope.launch { _effects.send(effect) }
|
||||
|
||||
private val signMutex = Mutex()
|
||||
|
||||
private val _responses = Channel<SignResult>()
|
||||
private fun setResponse(response: SignResult) = scope.launch { _responses.send(response) }
|
||||
|
||||
suspend fun signNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): SignResult {
|
||||
val result = try {
|
||||
signNostrEvent(publicKey = unsignedNostrEvent.pubKey, event = unsignedNostrEvent)
|
||||
} catch (error: SignatureException) {
|
||||
return SignResult.Rejected(error)
|
||||
}
|
||||
|
||||
return if (result != null) {
|
||||
SignResult.Signed(result)
|
||||
} else {
|
||||
signMutex.withLock {
|
||||
setEffect(NotarySideEffect.RequestSignature(unsignedNostrEvent))
|
||||
_responses.receive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verifySignature(nostrEvent: NostrEvent): Boolean {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
fun onSuccess(nostrEvent: NostrEvent) {
|
||||
setResponse(SignResult.Signed(nostrEvent))
|
||||
}
|
||||
|
||||
fun onFailure() {
|
||||
setResponse(SignResult.Rejected(SigningRejectedException()))
|
||||
}
|
||||
|
||||
private fun findNsecOrThrow(pubkey: String): String =
|
||||
runCatching {
|
||||
val npub = Hex.decode(pubkey).toNpub()
|
||||
credentialsStore.findOrThrow(npub = npub).nsec
|
||||
}.getOrNull() ?: throw SigningKeyNotFoundException()
|
||||
|
||||
private fun signNostrEvent(publicKey: String, event: UnsignedNostrEvent): NostrEvent? {
|
||||
val isExternalSignerLogin = runCatching {
|
||||
credentialsStore.isExternalSignerCredential(npub = publicKey.hexToNpubHrp())
|
||||
}.getOrDefault(false)
|
||||
|
||||
if (isExternalSignerLogin) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
return event.signOrThrow(nsec = findNsecOrThrow(publicKey))
|
||||
}
|
||||
|
||||
suspend fun signRelayListMetadata(userId: String, relays: List<RelayDTO>): SignResult {
|
||||
return signNostrEvent(
|
||||
unsignedNostrEvent = UnsignedNostrEvent(
|
||||
pubKey = userId,
|
||||
content = "",
|
||||
kind = RelayFeedsListEvent.KIND,
|
||||
tags = relays.map {
|
||||
arrayOf(
|
||||
"r",
|
||||
it.url,
|
||||
when {
|
||||
it.read -> "read"
|
||||
it.write -> "write"
|
||||
else -> ""
|
||||
}
|
||||
)
|
||||
}.toTypedArray() + listOf(UserAgent.CLIENT_NAME.asClientTag()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class NotarySideEffect {
|
||||
data class RequestSignature(val unsignedEvent: UnsignedNostrEvent) : NotarySideEffect()
|
||||
}
|
||||
|
||||
sealed class SignResult {
|
||||
data class Signed(val event: NostrEvent) : SignResult()
|
||||
data class Rejected(val error: SignatureException) : SignResult()
|
||||
|
||||
fun unwrapOrThrow(onFailure: ((SignatureException) -> Unit)? = null): NostrEvent =
|
||||
when (this) {
|
||||
is Rejected -> {
|
||||
onFailure?.invoke(this.error)
|
||||
throw this.error
|
||||
}
|
||||
|
||||
is Signed -> {
|
||||
this.event
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrNull(onFailure: ((SignatureException) -> Unit)? = null): NostrEvent? =
|
||||
when (this) {
|
||||
is Rejected -> {
|
||||
onFailure?.invoke(this.error)
|
||||
null
|
||||
}
|
||||
|
||||
is Signed -> {
|
||||
this.event
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrThrow(error: Throwable) =
|
||||
when (this) {
|
||||
is Rejected -> throw error
|
||||
is Signed -> this.event
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package ac.aux.compose.repository
|
||||
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
import ac.aux.compose.exceptions.SignatureException
|
||||
import ac.aux.compose.network.dto.RelayDTO
|
||||
import ac.aux.compose.network.relays.RelaysSocketManager
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import ac.aux.compose.exceptions.NostrPublishException
|
||||
|
||||
/**
|
||||
* As Seen in Primal
|
||||
*
|
||||
* https://github.com/PrimalHQ/primal-android-app/blob/ec22916b4e3472a20bade82d53957a7ef9b708b0/app/src/main/kotlin/net/primal/android/nostr/publish/NostrPublisher.kt#L20
|
||||
*/
|
||||
class NostrPublisherRepository(
|
||||
private val relaysSocketManager: RelaysSocketManager,
|
||||
private val nostrNotary: NostrNotaryRepository,
|
||||
private val cachingImportRepository: CachingImportRepository,
|
||||
) {
|
||||
val logger = Logger.withTag("NostrPublisherRepository")
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private fun importEvent(event: NostrEvent) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
cachingImportRepository.importEvents(events = listOf(event))
|
||||
}.onFailure { error ->
|
||||
logger.w(throwable = error) { "Failed to import event ${event.id} to caching server." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(NostrPublishException::class)
|
||||
private suspend fun publishAndImportEvent(signedNostrEvent: NostrEvent, outboxRelays: List<String> = emptyList()) {
|
||||
relaysSocketManager.publishEvent(signedNostrEvent)
|
||||
importEvent(signedNostrEvent)
|
||||
if (outboxRelays.isNotEmpty()) {
|
||||
runCatching {
|
||||
relaysSocketManager.publishEvent(
|
||||
nostrEvent = signedNostrEvent,
|
||||
relays = outboxRelays.map { RelayDTO(url = it, read = false, write = true) },
|
||||
)
|
||||
}.onFailure { error ->
|
||||
logger.w(throwable = error) { "Failed to publish to outbox relays." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(NostrPublishException::class, SignatureException::class)
|
||||
suspend fun signPublishImportNostrEvent(
|
||||
unsignedNostrEvent: UnsignedNostrEvent,
|
||||
outboxRelays: List<String>,
|
||||
): NostrEvent {
|
||||
val signedNostrEvent = nostrNotary.signNostrEvent(unsignedNostrEvent = unsignedNostrEvent).unwrapOrThrow()
|
||||
publishAndImportEvent(signedNostrEvent = signedNostrEvent, outboxRelays = outboxRelays)
|
||||
return signedNostrEvent
|
||||
}
|
||||
|
||||
|
||||
@Throws(NostrPublishException::class, SignatureException::class)
|
||||
suspend fun publishRelayList(userId: String, relays: List<RelayDTO>): NostrEvent {
|
||||
val signedNostrEvent = nostrNotary.signRelayListMetadata(userId = userId, relays = relays).unwrapOrThrow()
|
||||
relaysSocketManager.publishEvent(nostrEvent = signedNostrEvent, relays = relays)
|
||||
importEvent(signedNostrEvent)
|
||||
return signedNostrEvent
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package ac.aux.compose.ui.view.model
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.managers.SeedManager
|
||||
import ac.aux.compose.network.NostrEventBroadcaster
|
||||
import ac.aux.compose.network.relays.RelaysSocketManager
|
||||
import ac.aux.compose.network.sockets.NostrSocketClientFactory
|
||||
import ac.aux.compose.nostr.Relays
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.ui.view.state.NavigationUIState
|
||||
|
||||
Reference in New Issue
Block a user