Adds private outbox relays to save Draft events.

This commit is contained in:
Vitor Pamplona 2024-05-30 16:02:21 -04:00
parent 6ecb5ecc47
commit a38346c7d3
10 changed files with 510 additions and 20 deletions

View File

@ -89,6 +89,7 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.Price
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RelayAuthEvent
import com.vitorpamplona.quartz.events.ReportEvent
@ -233,12 +234,14 @@ class Account(
getNIP65RelayListFlow(),
getDMRelayListFlow(),
getSearchRelayListFlow(),
getPrivateOutboxRelayListFlow(),
userProfile().flow().relays.stateFlow,
) { nip65RelayList, dmRelayList, searchRelayList, userProfile ->
) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile ->
val baseRelaySet = activeRelays() ?: convertLocalRelays()
val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.toSet() ?: emptySet()
val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.toSet() ?: Constants.defaultSearchRelaySet
val nip65RelaySet = (nip65RelayList.note.event as? AdvertisedRelayListEvent)?.relays()
val privateOutboxRelaySet = (privateOutBox.note.event as? PrivateOutboxRelayListEvent)?.relays() ?: emptySet()
var mappedRelaySet =
baseRelaySet.map {
@ -270,6 +273,21 @@ class Account(
}
}
mappedRelaySet =
mappedRelaySet.map {
if (privateOutboxRelaySet.contains(it.url) == true) {
Relay(it.url, true, true, it.activeTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
} else {
it
}
}
privateOutboxRelaySet.forEach { newUrl ->
if (mappedRelaySet.filter { it.url == newUrl }.isEmpty()) {
mappedRelaySet = mappedRelaySet + Relay(newUrl, true, true, setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS))
}
}
mappedRelaySet =
mappedRelaySet.map {
if (localRelayServers.contains(it.url) == true) {
@ -1416,7 +1434,12 @@ class Account(
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
val newRelayList = getPrivateOutboxRelayList()?.relays()
if (newRelayList != null) {
Client.sendPrivately(draftEvent, newRelayList)
} else {
Client.send(draftEvent, relayList = relayList)
}
LocalCache.justConsume(draftEvent, null)
}
}
@ -1480,7 +1503,12 @@ class Account(
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
val newRelayList = getPrivateOutboxRelayList()?.relays()
if (newRelayList != null) {
Client.sendPrivately(draftEvent, newRelayList)
} else {
Client.send(draftEvent, relayList = relayList)
}
LocalCache.justConsume(draftEvent, null)
}
}
@ -1506,15 +1534,15 @@ class Account(
fun deleteDraft(draftTag: String) {
val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag)
LocalCache.getAddressableNoteIfExists(key)?.let {
val noteEvent = it.event
LocalCache.getAddressableNoteIfExists(key)?.let { note ->
val noteEvent = note.event
if (noteEvent is DraftEvent) {
noteEvent.createDeletedEvent(signer) {
Client.send(it)
Client.sendPrivately(it, relayList = note.relays.map { it.url })
LocalCache.justConsume(it, null)
}
}
delete(it)
delete(note)
}
}
@ -1564,7 +1592,12 @@ class Account(
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
val newRelayList = getPrivateOutboxRelayList()?.relays()
if (newRelayList != null) {
Client.sendPrivately(draftEvent, newRelayList)
} else {
Client.send(draftEvent, relayList = relayList)
}
LocalCache.justConsume(draftEvent, null)
}
}
@ -1657,7 +1690,12 @@ class Account(
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
val newRelayList = getPrivateOutboxRelayList()?.relays()
if (newRelayList != null) {
Client.sendPrivately(draftEvent, newRelayList)
} else {
Client.send(draftEvent, relayList = relayList)
}
LocalCache.justConsume(draftEvent, null)
}
}
@ -1711,7 +1749,12 @@ class Account(
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent)
val newRelayList = getPrivateOutboxRelayList()?.relays()
if (newRelayList != null) {
Client.sendPrivately(draftEvent, newRelayList)
} else {
Client.send(draftEvent)
}
LocalCache.justConsume(draftEvent, null)
}
}
@ -1758,7 +1801,12 @@ class Account(
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent)
val newRelayList = getPrivateOutboxRelayList()?.relays()
if (newRelayList != null) {
Client.sendPrivately(draftEvent, newRelayList)
} else {
Client.send(draftEvent)
}
LocalCache.justConsume(draftEvent, null)
}
}
@ -1832,7 +1880,12 @@ class Account(
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent ->
Client.send(draftEvent)
val newRelayList = getPrivateOutboxRelayList()?.relays()
if (newRelayList != null) {
Client.sendPrivately(draftEvent, newRelayList)
} else {
Client.send(draftEvent)
}
LocalCache.justConsume(draftEvent, null)
}
}
@ -1880,7 +1933,12 @@ class Account(
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it.msg, emptyList(), signer) { draftEvent ->
Client.send(draftEvent)
val newRelayList = getPrivateOutboxRelayList()?.relays()
if (newRelayList != null) {
Client.sendPrivately(draftEvent, newRelayList)
} else {
Client.send(draftEvent)
}
LocalCache.justConsume(draftEvent, null)
}
}
@ -2655,11 +2713,7 @@ class Account(
fun saveDMRelayList(dmRelays: List<String>) {
if (!isWriteable()) return
val relayListForDMs =
LocalCache.getOrCreateAddressableNote(
ChatMessageRelayListEvent.createAddressATag(signer.pubKey),
).event as? ChatMessageRelayListEvent
val relayListForDMs = getDMRelayList()
if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) {
ChatMessageRelayListEvent.updateRelayList(
earlierVersion = relayListForDMs,
@ -2680,6 +2734,45 @@ class Account(
}
}
fun getPrivateOutboxRelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
PrivateOutboxRelayListEvent.createAddressATag(signer.pubKey),
)
}
fun getPrivateOutboxRelayListFlow(): StateFlow<NoteState> {
return getPrivateOutboxRelayListNote().flow().metadata.stateFlow
}
fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? {
return getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent
}
fun savePrivateOutboxRelayList(relays: List<String>) {
if (!isWriteable()) return
val relayListForPrivateOutbox = getPrivateOutboxRelayList()
if (relayListForPrivateOutbox != null && !relayListForPrivateOutbox.cachedPrivateTags().isNullOrEmpty()) {
PrivateOutboxRelayListEvent.updateRelayList(
earlierVersion = relayListForPrivateOutbox,
relays = relays,
signer = signer,
) {
Client.send(it)
LocalCache.justConsume(it, null)
}
} else {
PrivateOutboxRelayListEvent.createFromScratch(
relays = relays,
signer = signer,
) {
Client.send(it)
LocalCache.justConsume(it, null)
}
}
}
fun getSearchRelayListNote(): AddressableNote {
return LocalCache.getOrCreateAddressableNote(
SearchRelayListEvent.createAddressATag(signer.pubKey),

View File

@ -102,6 +102,7 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RecommendRelayEvent
import com.vitorpamplona.quartz.events.RelaySetEvent
@ -929,6 +930,13 @@ object LocalCache {
consumeBaseReplaceable(event, relay)
}
private fun consume(
event: PrivateOutboxRelayListEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
private fun consume(
event: SearchRelayListEvent,
relay: Relay?,
@ -2565,6 +2573,7 @@ object LocalCache {
is NNSEvent -> comsume(event, relay)
is OtsEvent -> consume(event, relay)
is PrivateDmEvent -> consume(event, relay)
is PrivateOutboxRelayListEvent -> consume(event, relay)
is PinListEvent -> consume(event, relay)
is PeopleListEvent -> consume(event, relay)
is PollNoteEvent -> consume(event, relay)

View File

@ -58,6 +58,7 @@ import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.RepostEvent
@ -104,7 +105,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
types = COMMON_FEED_TYPES,
filter =
JsonFilter(
kinds = listOf(StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND, SearchRelayListEvent.KIND),
kinds = listOf(StatusEvent.KIND, AdvertisedRelayListEvent.KIND, ChatMessageRelayListEvent.KIND, SearchRelayListEvent.KIND, PrivateOutboxRelayListEvent.KIND),
authors = listOf(account.userProfile().pubkeyHex),
limit = 10,
),
@ -123,6 +124,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
ContactListEvent.KIND,
AdvertisedRelayListEvent.KIND,
ChatMessageRelayListEvent.KIND,
PrivateOutboxRelayListEvent.KIND,
SearchRelayListEvent.KIND,
FileServersEvent.KIND,
MuteListEvent.KIND,
@ -285,6 +287,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
if (LocalCache.justVerify(event)) {
when (event) {
is PrivateOutboxRelayListEvent -> {
val note = LocalCache.getAddressableNoteIfExists(event.addressTag())
val noteEvent = note?.event
if (noteEvent == null || event.createdAt > noteEvent.createdAt()) {
event.privateTags(account.signer) {
LocalCache.justConsume(event, relay)
}
}
}
is DraftEvent -> {
// Avoid decrypting over and over again if the event already exist.

View File

@ -74,6 +74,9 @@ fun AllRelayListView(
val homeFeedState by nip65ViewModel.homeRelays.collectAsStateWithLifecycle()
val notifFeedState by nip65ViewModel.notificationRelays.collectAsStateWithLifecycle()
val privateOutboxViewModel: PrivateOutboxRelayListViewModel = viewModel()
val privateOutboxFeedState by privateOutboxViewModel.relays.collectAsStateWithLifecycle()
val searchViewModel: SearchRelayListViewModel = viewModel()
val searchFeedState by searchViewModel.relays.collectAsStateWithLifecycle()
@ -86,6 +89,7 @@ fun AllRelayListView(
nip65ViewModel.load(accountViewModel.account)
searchViewModel.load(accountViewModel.account)
localViewModel.load(accountViewModel.account)
privateOutboxViewModel.load(accountViewModel.account)
}
Dialog(
@ -115,6 +119,7 @@ fun AllRelayListView(
nip65ViewModel.create()
searchViewModel.create()
localViewModel.create()
privateOutboxViewModel.create()
onClose()
},
true,
@ -127,6 +132,11 @@ fun AllRelayListView(
CloseButton(
onPress = {
kind3ViewModel.clear()
dmViewModel.clear()
nip65ViewModel.clear()
searchViewModel.clear()
localViewModel.clear()
privateOutboxViewModel.clear()
onClose()
},
)
@ -177,6 +187,14 @@ fun AllRelayListView(
}
renderDMItems(dmFeedState, dmViewModel, accountViewModel, onClose, nav)
item {
SettingsCategory(
stringResource(R.string.private_outbox_section),
stringResource(R.string.private_outbox_section_explainer),
)
}
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, onClose, nav)
item {
SettingsCategoryWithButton(
stringResource(R.string.search_section),

View File

@ -59,7 +59,7 @@ fun LazyListScope.renderLocalItems(
onClose: () -> Unit,
nav: (String) -> Unit,
) {
itemsIndexed(feedState, key = { _, item -> "DM" + item.url }) { index, item ->
itemsIndexed(feedState, key = { _, item -> "Local" + item.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteRelay(item) },

View File

@ -0,0 +1,77 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@Composable
fun PrivateOutboxRelayList(
postViewModel: PrivateOutboxRelayListViewModel,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
nav: (String) -> Unit,
) {
val feedState by postViewModel.relays.collectAsStateWithLifecycle()
Row(verticalAlignment = Alignment.CenterVertically) {
LazyColumn(
contentPadding = FeedPadding,
) {
renderPrivateOutboxItems(feedState, postViewModel, accountViewModel, onClose, nav)
}
}
}
fun LazyListScope.renderPrivateOutboxItems(
feedState: List<BasicRelaySetupInfo>,
postViewModel: PrivateOutboxRelayListViewModel,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
nav: (String) -> Unit,
) {
itemsIndexed(feedState, key = { _, item -> "Outbox" + item.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteRelay(item) },
accountViewModel = accountViewModel,
) {
onClose()
nav(it)
}
}
item {
Spacer(modifier = StdVertSpacer)
RelayUrlEditField { postViewModel.addRelay(it) }
}
}

View File

@ -0,0 +1,109 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.RelayPool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class PrivateOutboxRelayListViewModel : ViewModel() {
private lateinit var account: Account
private val _relays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
val relays = _relays.asStateFlow()
fun load(account: Account) {
this.account = account
clear()
loadRelayDocuments()
}
fun create() {
viewModelScope.launch(Dispatchers.IO) {
account.savePrivateOutboxRelayList(_relays.value.map { it.url })
clear()
}
}
fun loadRelayDocuments() {
viewModelScope.launch(Dispatchers.IO) {
_relays.value.forEach { item ->
Nip11CachedRetriever.loadRelayInfo(
dirtyUrl = item.url,
onInfo = {
togglePaidRelay(item, it.limitation?.payment_required ?: false)
},
onError = { url, errorCode, exceptionMessage -> },
)
}
}
}
fun clear() {
_relays.update {
val relayList = account.getPrivateOutboxRelayList()?.relays() ?: emptyList()
relayList.map { relayUrl ->
val liveRelay = RelayPool.getRelay(relayUrl)
val errorCounter = liveRelay?.errorCounter ?: 0
val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0
val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0
val spamCounter = liveRelay?.spamCounter ?: 0
BasicRelaySetupInfo(
relayUrl,
errorCounter,
eventDownloadCounter,
eventUploadCounter,
spamCounter,
)
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed()
}
}
fun addRelay(relay: BasicRelaySetupInfo) {
if (relays.value.any { it.url == relay.url }) return
_relays.update { it.plus(relay) }
}
fun deleteRelay(relay: BasicRelaySetupInfo) {
_relays.update { it.minus(relay) }
}
fun deleteAll() {
_relays.update { relays -> emptyList() }
}
fun togglePaidRelay(
relay: BasicRelaySetupInfo,
paid: Boolean,
) {
_relays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
}
}

View File

@ -825,6 +825,8 @@
<string name="public_notif_section_explainer">This relay type receives all replies, comments, likes and zaps to your posts. Insert between 13 relays and make sure they accept posts from anyone.</string>
<string name="private_inbox_section">DM Inbox Relays</string>
<string name="private_inbox_section_explainer">Insert between 13 relays to serve as your private inbox. Others will use these relays to send DMs to you. DM Inbox relays should accept any message from anyone, but only allow you to download them. Good options are:\n - inbox.nostr.wine (paid)\n - you.nostr1.com (personal relays - paid)</string>
<string name="private_outbox_section">Private Relays</string>
<string name="private_outbox_section_explainer">Insert between 13 relays to store events no one else can see, like your Drafts and/or app settings. Ideally, these relays are either local or require authentication before downloading each user\'s content.</string>
<string name="kind_3_section">General Relays</string>
<string name="kind_3_section_description">Amethyst uses these relays to download posts for you.</string>
<string name="search_section">Search Relays</string>

View File

@ -124,6 +124,7 @@ class EventFactory {
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
PollNoteEvent.KIND -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
PrivateDmEvent.KIND -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
PrivateOutboxRelayListEvent.KIND -> PrivateOutboxRelayListEvent(id, pubKey, createdAt, tags, content, sig)
ReactionEvent.KIND -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
RecommendRelayEvent.KIND -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig)
RelayAuthEvent.KIND -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig)

View File

@ -0,0 +1,169 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class PrivateOutboxRelayListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient private var privateTagsCache: Array<Array<String>>? = null
override fun dTag() = FIXED_D_TAG
fun relays(): List<String>? {
return tags.mapNotNull {
if (it.size > 1 && it[0] == "relay") {
it[1]
} else {
null
}
}.plus(
privateTagsCache?.mapNotNull {
if (it.size > 1 && it[0] == "relay") {
it[1]
} else {
null
}
} ?: emptyList(),
).ifEmpty { null }
}
fun cachedPrivateTags(): Array<Array<String>>? {
return privateTagsCache
}
fun privateTags(
signer: NostrSigner,
onReady: (Array<Array<String>>) -> Unit,
) {
if (content.isEmpty()) {
onReady(emptyArray())
return
}
privateTagsCache?.let {
onReady(it)
return
}
try {
signer.nip44Decrypt(content, pubKey) {
privateTagsCache = mapper.readValue<Array<Array<String>>>(it)
privateTagsCache?.let { onReady(it) }
}
} catch (e: Throwable) {
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
}
}
companion object {
const val KIND = 10013
const val FIXED_D_TAG = ""
val TAGS = arrayOf(arrayOf("alt", "Relay list to store private content from this author"))
fun createAddressATag(pubKey: HexKey): ATag {
return ATag(KIND, pubKey, FIXED_D_TAG, null)
}
fun createAddressTag(pubKey: HexKey): String {
return ATag.assembleATag(KIND, pubKey, FIXED_D_TAG)
}
fun encryptTags(
privateTags: Array<Array<String>>? = null,
signer: NostrSigner,
onReady: (String) -> Unit,
) {
val msg = mapper.writeValueAsString(privateTags)
signer.nip44Encrypt(
msg,
signer.pubKey,
onReady,
)
}
fun createTagArray(relays: List<String>): Array<Array<String>> {
return relays.map {
arrayOf("relay", it)
}.toTypedArray()
}
fun updateRelayList(
earlierVersion: PrivateOutboxRelayListEvent,
relays: List<String>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PrivateOutboxRelayListEvent) -> Unit,
) {
val tags =
earlierVersion.privateTagsCache?.filter { it[0] != "relay" }?.plus(
relays.map {
arrayOf("relay", it)
},
)?.toTypedArray() ?: emptyArray()
encryptTags(tags, signer) {
signer.sign<PrivateOutboxRelayListEvent>(createdAt, KIND, TAGS, it) {
it.privateTagsCache = tags
onReady(it)
}
}
}
fun createFromScratch(
relays: List<String>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PrivateOutboxRelayListEvent) -> Unit,
) {
create(relays, signer, createdAt, onReady)
}
fun create(
relays: List<String>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PrivateOutboxRelayListEvent) -> Unit,
) {
val privateTagArray = createTagArray(relays)
encryptTags(privateTagArray, signer) { privateTags ->
signer.sign<PrivateOutboxRelayListEvent>(createdAt, KIND, TAGS, privateTags) {
it.privateTagsCache = privateTagArray
onReady(it)
}
}
}
}
}