Minimizes the dependency on the Account further by moving Tor settings to a tor Privacy State

This commit is contained in:
Vitor Pamplona
2025-07-21 19:09:06 -04:00
parent 95c6dcce5e
commit 8e6d71e422
29 changed files with 305 additions and 342 deletions
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
import com.vitorpamplona.amethyst.model.privacyOptions.PrivacyState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
import com.vitorpamplona.amethyst.model.serverList.MergedServerListState
@@ -79,9 +80,8 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState
import com.vitorpamplona.amethyst.model.torState.TorRelayState
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder
import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder
import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
@@ -118,7 +118,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirst
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses
@@ -130,7 +129,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.references.references
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip04Dm.messages.reply
@@ -302,14 +300,17 @@ class Account(
val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey)
val otsResolver: OtsResolver =
OtsResolverBuilder().build(
val privacyState = PrivacyState(settings)
val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope)
val otsResolverBuilder: OkHttpOtsResolverBuilder =
OkHttpOtsResolverBuilder(
Amethyst.instance.okHttpClients,
::shouldUseTorForMoneyOperations,
privacyState::shouldUseTorForMoneyOperations,
Amethyst.instance.otsBlockHeightCache,
)
val otsState = OtsState(signer, cache, otsResolver, scope, settings)
val otsState = OtsState(signer, cache, otsResolverBuilder, scope, settings)
val feedDecryptionCaches =
FeedDecryptionCaches(
@@ -389,8 +390,6 @@ class Account(
*/
val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope)
fun isWriteable(): Boolean = settings.isWriteable()
suspend fun updateWarnReports(warnReports: Boolean): Boolean {
@@ -463,67 +462,15 @@ class Account(
private suspend fun sendNewAppSpecificData() = sendMyPublicAndPrivateOutbox(appSpecific.saveNewAppSpecificData())
suspend fun sendNewUserMetadata(
name: String? = null,
picture: String? = null,
banner: String? = null,
website: String? = null,
pronouns: String? = null,
about: String? = null,
nip05: String? = null,
lnAddress: String? = null,
lnURL: String? = null,
twitter: String? = null,
mastodon: String? = null,
github: String? = null,
) {
if (!isWriteable()) return
val userMetadataEvent =
userMetadata.sendNewUserMetadata(
name,
picture,
banner,
website,
pronouns,
about,
nip05,
lnAddress,
lnURL,
twitter,
mastodon,
github,
)
sendLiterallyEverywhere(userMetadataEvent)
}
fun reactionTo(
note: Note,
reaction: String,
): List<Note> = note.reactedBy(userProfile(), reaction)
fun hasBoosted(note: Note): Boolean = boostsTo(note).isNotEmpty()
fun boostsTo(note: Note): List<Note> = note.boostedBy(userProfile())
fun hasReacted(
note: Note,
reaction: String,
): Boolean = note.hasReacted(userProfile(), reaction)
suspend fun reactTo(
note: Note,
reaction: String,
) = ReactionAction.reactTo(
note,
reaction,
userProfile(),
signer,
onPublic = {
client.send(it, computeMyReactionToNote(note, it))
cache.justConsumeMyOwnEvent(it)
},
note = note,
reaction = reaction,
by = userProfile(),
signer = signer,
onPublic = ::sendAutomatic,
onPrivate = ::broadcastPrivately,
)
@@ -534,18 +481,15 @@ class Account(
zapType: LnZapEvent.ZapType,
toUser: User?,
additionalRelays: Set<NormalizedRelayUrl>? = null,
): LnZapRequestEvent {
val relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet())
return LnZapRequestEvent.create(
event,
relays = relays.mapTo(mutableSetOf()) { it.url },
signer,
pollOption,
message,
zapType,
toUser?.pubkeyHex,
)
}
) = LnZapRequestEvent.create(
zappedEvent = event,
relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()),
signer = signer,
pollOption = pollOption,
message = message,
zapType = zapType,
toUserPubHex = toUser?.pubkeyHex,
)
suspend fun calculateIfNoteWasZappedByAccount(
zappedNote: Note?,
@@ -570,12 +514,10 @@ class Account(
message: String = "",
zapType: LnZapEvent.ZapType,
): LnZapRequestEvent {
val relays = nip65RelayList.inboxFlow.value + user.inboxRelays()
val zapRequest =
LnZapRequestEvent.create(
userHex = user.pubkeyHex,
relays = relays,
relays = nip65RelayList.inboxFlow.value + user.inboxRelays(),
signer = signer,
message = message,
zapType = zapType,
@@ -596,9 +538,7 @@ class Account(
type: ReportType,
) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, userProfile(), signer))
suspend fun delete(note: Note) {
delete(listOf(note))
}
suspend fun delete(note: Note) = delete(listOf(note))
suspend fun delete(notes: List<Note>) {
if (!isWriteable()) return
@@ -627,10 +567,7 @@ class Account(
if (!isWriteable()) return
if (event.pubKey != signer.pubKey) return
val deletionEvent =
signer.sign(
DeletionEvent.build(listOf(event)),
)
val deletionEvent = signer.sign(DeletionEvent.build(listOf(event)))
client.send(deletionEvent, outboxRelays.flow.value + additionalRelays)
cache.justConsumeMyOwnEvent(deletionEvent)
}
@@ -1822,104 +1759,6 @@ class Account(
fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME)
fun shouldUseTorForImageDownload(url: String) =
shouldUseTorFor(
url,
settings.torSettings.torType.value,
settings.torSettings.imagesViaTor.value,
)
fun shouldUseTorFor(
url: String,
torType: TorType,
imagesViaTor: Boolean,
) = when (torType) {
TorType.OFF -> false
TorType.INTERNAL -> shouldUseTor(url, imagesViaTor)
TorType.EXTERNAL -> shouldUseTor(url, imagesViaTor)
}
private fun shouldUseTor(
normalizedUrl: String,
final: Boolean,
): Boolean =
if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) {
false
} else if (RelayUrlNormalizer.isOnion(normalizedUrl)) {
true
} else {
final
}
fun shouldUseTorForVideoDownload() =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> settings.torSettings.videosViaTor.value
TorType.EXTERNAL -> settings.torSettings.videosViaTor.value
}
fun shouldUseTorForVideoDownload(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
}
fun shouldUseTorForPreviewUrl(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
}
fun shouldUseTorForTrustedRelays() =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value
TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value
}
fun shouldUseTorForClean(relay: NormalizedRelayUrl) = torRelayState.flow.value.useTor(relay)
private fun checkLocalHostOnionAndThen(
url: String,
final: Boolean,
): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final)
private fun checkLocalHostOnionAndThen(
normalizedUrl: String,
isOnionRelaysActive: Boolean,
final: Boolean,
): Boolean =
if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) {
false
} else if (RelayUrlNormalizer.isOnion(normalizedUrl)) {
isOnionRelaysActive
} else {
final
}
fun shouldUseTorForMoneyOperations(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
}
fun shouldUseTorForNIP05(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
}
fun shouldUseTorForNIP96(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
}
init {
Log.d("AccountRegisterObservers", "Init")
@@ -76,7 +76,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers
import com.vitorpamplona.quartz.nip01Core.verify
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.VerificationState
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
@@ -2102,7 +2102,7 @@ object LocalCache : ILocalCache {
suspend fun findEarliestOtsForNote(
note: Note,
resolverBuilder: () -> OtsResolver,
resolverBuilder: OtsResolverBuilder,
): Long? {
checkNotInMainThread()
@@ -745,14 +745,14 @@ open class Note(
fun hasReacted(
loggedIn: User,
content: String,
): Boolean = reactedBy(loggedIn, content).isNotEmpty()
): Boolean = allReactionsOfContentByAuthor(loggedIn, content).isNotEmpty()
fun reactedBy(
fun allReactionsOfContentByAuthor(
loggedIn: User,
content: String,
): List<Note> = reactions[content]?.filter { it.author == loggedIn } ?: emptyList()
fun reactedBy(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
fun allReactionsByAuthor(loggedIn: User): List<String> = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key }
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val fiveMinsAgo = TimeUtils.fiveMinutesAgo()
@@ -24,10 +24,10 @@ import android.util.Log
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.update
import java.util.Base64
@@ -35,7 +35,7 @@ import java.util.Base64
class OtsState(
val signer: NostrSigner,
val cache: LocalCache,
val otsResolver: OtsResolver,
val otsResolver: OkHttpOtsResolverBuilder,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
@@ -43,7 +43,7 @@ class OtsState(
Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations")
return settings.pendingAttestations.value.toList().mapNotNull { (key, value) ->
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver)
val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver.build())
if (otsState != null) {
val hint = cache.getNoteIfExists(key)?.toEventHint<Event>()
@@ -75,6 +75,15 @@ class OtsState(
val id = note.event?.id ?: note.idHex
settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver)))
settings.addPendingAttestation(
id = id,
stamp =
Base64.getEncoder().encodeToString(
OtsEvent.stamp(
id,
otsResolver.build(),
),
),
)
}
}
@@ -0,0 +1,125 @@
/**
* Copyright (c) 2025 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.model.privacyOptions
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
class PrivacyState(
val settings: AccountSettings,
) {
fun shouldUseTorForImageDownload(url: String) =
shouldUseTorFor(
url,
settings.torSettings.torType.value,
settings.torSettings.imagesViaTor.value,
)
fun shouldUseTorFor(
url: String,
torType: TorType,
imagesViaTor: Boolean,
) = when (torType) {
TorType.OFF -> false
TorType.INTERNAL -> shouldUseTor(url, imagesViaTor)
TorType.EXTERNAL -> shouldUseTor(url, imagesViaTor)
}
private fun shouldUseTor(
normalizedUrl: String,
final: Boolean,
): Boolean =
if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) {
false
} else if (RelayUrlNormalizer.isOnion(normalizedUrl)) {
true
} else {
final
}
fun shouldUseTorForVideoDownload() =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> settings.torSettings.videosViaTor.value
TorType.EXTERNAL -> settings.torSettings.videosViaTor.value
}
fun shouldUseTorForVideoDownload(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value)
}
fun shouldUseTorForPreviewUrl(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value)
}
fun shouldUseTorForTrustedRelays() =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value
TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value
}
private fun checkLocalHostOnionAndThen(
url: String,
final: Boolean,
): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final)
private fun checkLocalHostOnionAndThen(
normalizedUrl: String,
isOnionRelaysActive: Boolean,
final: Boolean,
): Boolean =
if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) {
false
} else if (RelayUrlNormalizer.isOnion(normalizedUrl)) {
isOnionRelaysActive
} else {
final
}
fun shouldUseTorForMoneyOperations(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value)
}
fun shouldUseTorForNIP05(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value)
}
fun shouldUseTorForUploads(url: String) =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value)
}
}
@@ -65,10 +65,6 @@ class ChatroomList(
}
}
fun createChatroom(withKey: ChatroomKey) {
getOrCreatePrivateChatroom(withKey)
}
fun removeMessage(
user: User,
msg: Note,
@@ -115,4 +115,6 @@ class TorRelayState(
dmRelayList = dmRelayState.flow.value,
),
)
fun shouldUseTorForClean(relay: NormalizedRelayUrl) = flow.value.useTor(relay)
}
@@ -22,8 +22,13 @@ package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
class OtsResolverBuilder {
class OkHttpOtsResolverBuilder(
val okHttpClients: DualHttpClientManager,
val shouldUseTorForUrl: (String) -> Boolean,
val cache: OtsBlockHeightCache,
) : OtsResolverBuilder {
fun getAPI(usingTor: Boolean) =
if (usingTor) {
OkHttpBitcoinExplorer.MEMPOOL_API_URL
@@ -31,22 +36,20 @@ class OtsResolverBuilder {
OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL
}
fun build(
okHttpClients: DualHttpClientManager,
shouldUseTorForUrl: (String) -> Boolean,
cache: OtsBlockHeightCache,
): OtsResolver {
override fun build(): OtsResolver {
val shouldUseTor = shouldUseTorForUrl(OkHttpBitcoinExplorer.MEMPOOL_API_URL)
return OtsResolver(
OkHttpBitcoinExplorer(
getAPI(shouldUseTor),
okHttpClients.getHttpClient(shouldUseTor),
cache,
),
OkHttpCalendarBuilder {
okHttpClients.getHttpClient(shouldUseTorForUrl(it))
},
explorer =
OkHttpBitcoinExplorer(
baseAPI = getAPI(usingTor = shouldUseTor),
client = okHttpClients.getHttpClient(shouldUseTor),
cache = cache,
),
calendarBuilder =
OkHttpCalendarBuilder {
okHttpClients.getHttpClient(shouldUseTorForUrl(it))
},
)
}
}
@@ -152,7 +152,7 @@ class UploadOrchestrator {
alt = alt,
sensitiveContent = contentWarningReason,
serverBaseUrl = serverBaseUrl,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
onProgress = { percent: Float ->
updateState(0.2 + (0.2 * percent), UploadingState.Uploading)
},
@@ -165,7 +165,7 @@ class UploadOrchestrator {
localContentType = contentType,
originalContentType = contentTypeForResult,
originalHash = originalHash,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
)
} catch (_: SignerExceptions.ReadOnlyException) {
error(R.string.login_with_a_private_key_to_be_able_to_upload)
@@ -198,7 +198,7 @@ class UploadOrchestrator {
alt = alt,
sensitiveContent = contentWarningReason,
serverBaseUrl = serverBaseUrl,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
httpAuth = account::createBlossomUploadAuth,
context = context,
)
@@ -206,7 +206,7 @@ class UploadOrchestrator {
verifyHeader(
uploadResult = result,
localContentType = contentType,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
originalHash = originalHash,
originalContentType = contentTypeForResult,
)
@@ -58,10 +58,11 @@ fun NewUserMetadataScreen(
accountViewModel: AccountViewModel,
) {
val postViewModel: NewUserMetadataViewModel = viewModel()
postViewModel.init(accountViewModel)
val context = LocalContext.current
LaunchedEffect(Unit) {
postViewModel.load(accountViewModel.account)
LaunchedEffect(accountViewModel) {
postViewModel.load()
}
Scaffold(
@@ -25,7 +25,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
@@ -35,17 +34,17 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.coroutines.cancellation.CancellationException
class NewUserMetadataViewModel : ViewModel() {
private lateinit var accountViewModel: AccountViewModel
private lateinit var account: Account
// val userName = mutableStateOf("")
@@ -68,9 +67,12 @@ class NewUserMetadataViewModel : ViewModel() {
var isUploadingImageForPicture by mutableStateOf(false)
var isUploadingImageForBanner by mutableStateOf(false)
fun load(account: Account) {
this.account = account
fun init(accountViewModel: AccountViewModel) {
this.accountViewModel = accountViewModel
this.account = accountViewModel.account
}
fun load() {
account.userProfile().let {
// userName.value = it.bestUsername() ?: ""
displayName.value = it.info?.bestName() ?: ""
@@ -100,21 +102,25 @@ class NewUserMetadataViewModel : ViewModel() {
fun create() {
// Tries to not delete any existing attribute that we do not work with.
viewModelScope.launch(Dispatchers.IO) {
account.sendNewUserMetadata(
name = displayName.value,
picture = picture.value,
banner = banner.value,
website = website.value,
pronouns = pronouns.value,
about = about.value,
nip05 = nip05.value,
lnAddress = lnAddress.value,
lnURL = lnURL.value,
twitter = twitter.value,
mastodon = mastodon.value,
github = github.value,
)
accountViewModel.runIOCatching {
val metadata =
account.userMetadata.sendNewUserMetadata(
name = displayName.value,
picture = picture.value,
banner = banner.value,
website = website.value,
pronouns = pronouns.value,
about = about.value,
nip05 = nip05.value,
lnAddress = lnAddress.value,
lnURL = lnURL.value,
twitter = twitter.value,
mastodon = mastodon.value,
github = github.value,
)
account.sendLiterallyEverywhere(metadata)
clear()
}
}
@@ -139,7 +145,7 @@ class NewUserMetadataViewModel : ViewModel() {
context: Context,
onError: (String, String) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
accountViewModel.runIOCatching {
upload(
uri,
context,
@@ -155,7 +161,7 @@ class NewUserMetadataViewModel : ViewModel() {
context: Context,
onError: (String, String) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
accountViewModel.runIOCatching {
upload(
uri,
context,
@@ -187,7 +193,7 @@ class NewUserMetadataViewModel : ViewModel() {
alt = null,
sensitiveContent = null,
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
onProgress = {},
httpAuth = account::createHTTPAuthorization,
context = context,
@@ -200,7 +206,7 @@ class NewUserMetadataViewModel : ViewModel() {
alt = null,
sensitiveContent = null,
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
httpAuth = account::createBlossomUploadAuth,
context = context,
)
@@ -79,7 +79,7 @@ fun routeFor(
}
} else if (innerEvent is ChatroomKeyable) {
val room = innerEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
loggedIn.chatroomList.createChatroom(room)
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
return Route.Room(room)
} else if (innerEvent is AddressableEvent) {
return Route.Note(noteEvent.aTag().toTag())
@@ -104,7 +104,7 @@ fun routeFor(
}
} else if (noteEvent is ChatroomKeyable) {
val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex)
loggedIn.chatroomList.createChatroom(room)
loggedIn.chatroomList.getOrCreatePrivateChatroom(room)
return Route.Room(room)
} else if (noteEvent is CommunityDefinitionEvent) {
return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag())
@@ -169,7 +169,7 @@ fun routeToMessage(
draftId: HexKey? = null,
account: Account,
): Route {
account.chatroomList.createChatroom(room)
account.chatroomList.getOrCreatePrivateChatroom(room)
return Route.Room(room, draftMessage, replyId, draftId)
}
@@ -1355,7 +1355,7 @@ fun ReactionChoicePopup(
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
val reactions by accountViewModel.reactionChoicesFlow().collectAsStateWithLifecycle()
val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() }
val toRemove = remember { baseNote.allReactionsByAuthor(accountViewModel.userProfile()).toImmutableSet() }
Popup(
alignment = Alignment.BottomCenter,
@@ -623,7 +623,7 @@ open class CommentPostViewModel :
iMetaAttachments.downloadAndPrepare(item.link.url) {
Amethyst.Companion.instance.okHttpClients
.getHttpClient(
accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url)
accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url)
?: false,
)
}
@@ -98,7 +98,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -266,13 +265,6 @@ class AccountViewModel(
fun userProfile(): User = account.userProfile()
suspend fun reactTo(
note: Note,
reaction: String,
) {
account.reactTo(note, reaction)
}
fun <T : Event> observeByETag(
kind: Int,
eTag: HexKey,
@@ -287,8 +279,8 @@ class AccountViewModel(
note: Note,
reaction: String,
) {
viewModelScope.launch(Dispatchers.IO) {
val currentReactions = account.reactionTo(note, reaction)
runIOCatching {
val currentReactions = note.allReactionsOfContentByAuthor(userProfile(), reaction)
if (currentReactions.isNotEmpty()) {
account.delete(currentReactions)
} else {
@@ -298,14 +290,8 @@ class AccountViewModel(
}
fun reactToOrDelete(note: Note) {
viewModelScope.launch(Dispatchers.IO) {
val reaction = reactionChoices().first()
if (hasReactedTo(note, reaction)) {
deleteReactionTo(note, reaction)
} else {
reactTo(note, reaction)
}
}
val reaction = reactionChoices().first()
reactToOrDelete(note, reaction)
}
@Immutable
@@ -402,24 +388,6 @@ class AccountViewModel(
noteMustShowExpandButtonFlows.put(note, it)
}
fun hasReactedTo(
baseNote: Note,
reaction: String,
): Boolean = account.hasReacted(baseNote, reaction)
suspend fun deleteReactionTo(
note: Note,
reaction: String,
) {
account.delete(account.reactionTo(note, reaction))
}
fun hasBoosted(baseNote: Note): Boolean = account.hasBoosted(baseNote)
fun deleteBoostsTo(note: Note) {
runIOCatching { account.delete(account.boostsTo(note)) }
}
suspend fun calculateIfNoteWasZappedByAccount(
zappedNote: Note,
onWasZapped: (Boolean) -> Unit,
@@ -977,7 +945,7 @@ class AccountViewModel(
.verifyNip05(
nip05,
okHttpClient = {
app.okHttpClients.getHttpClient(account.shouldUseTorForNIP05(it))
app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForNIP05(it))
},
onSuccess = {
// Marks user as verified
@@ -1103,7 +1071,7 @@ class AccountViewModel(
) {
onResult(
withContext(Dispatchers.Default) {
LocalCache.findEarliestOtsForNote(note, account::otsResolver)
LocalCache.findEarliestOtsForNote(note, account.otsResolverBuilder)
},
)
}
@@ -1228,21 +1196,7 @@ class AccountViewModel(
}
}
fun createChatRoomFor(
user: User,
then: (ChatroomKey) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex))
account.chatroomList.createChatroom(withKey)
then(withKey)
}
}
fun setTorSettings(newTorSettings: TorSettings) =
viewModelScope.launch(Dispatchers.IO) {
account.settings.setTorSettings(newTorSettings)
}
fun setTorSettings(newTorSettings: TorSettings) = runIOCatching { account.settings.setTorSettings(newTorSettings) }
class Factory(
val accountSettings: AccountSettings,
@@ -1357,8 +1311,11 @@ class AccountViewModel(
}
if (isWriteable()) {
if (hasBoosted(baseNote)) {
deleteBoostsTo(baseNote)
val boosts = baseNote.boostedBy(userProfile())
if (boosts.isNotEmpty()) {
runIOCatching {
account.delete(boosts)
}
} else {
onMore()
}
@@ -1496,21 +1453,21 @@ class AccountViewModel(
}
}
fun proxyPortFor(url: String): Int? = app.okHttpClients.getCurrentProxyPort(account.shouldUseTorForVideoDownload(url))
fun proxyPortFor(url: String): Int? = app.okHttpClients.getCurrentProxyPort(account.privacyState.shouldUseTorForVideoDownload(url))
fun okHttpClientForNip96(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(url))
fun okHttpClientForNip96(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(url))
fun okHttpClientForImage(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForImageDownload(url))
fun okHttpClientForImage(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForImageDownload(url))
fun okHttpClientForVideo(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForVideoDownload(url))
fun okHttpClientForVideo(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForVideoDownload(url))
fun okHttpClientForMoney(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForMoneyOperations(url))
fun okHttpClientForMoney(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForMoneyOperations(url))
fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForPreviewUrl(url))
fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForPreviewUrl(url))
fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForClean(url))
fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = app.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(url))
fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForTrustedRelays())
fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForTrustedRelays())
fun dataSources() = app.sources
@@ -85,7 +85,7 @@ class DecryptAndIndexProcessor(
is OtsEvent -> {
// verifies new OTS upon arrival
Amethyst.instance.otsVerifCache.cacheVerify(event, account::otsResolver)
Amethyst.instance.otsVerifCache.cacheVerify(event, account.otsResolverBuilder)
}
is DraftEvent -> {
@@ -130,7 +130,7 @@ fun SetProxyDeterminator(accountViewModel: AccountViewModel) {
@Composable
fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) {
LaunchedEffect(accountViewModel) {
Amethyst.instance.setImageLoader(accountViewModel.account::shouldUseTorForImageDownload)
Amethyst.instance.setImageLoader(accountViewModel.account.privacyState::shouldUseTorForImageDownload)
}
}
@@ -640,7 +640,7 @@ class ChatNewMessageViewModel :
viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare(item.link.url) {
Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.shouldUseTorForImageDownload(item.link.url))
Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url))
}
}
@@ -210,7 +210,7 @@ class ChannelMetadataViewModel : ViewModel() {
alt = null,
sensitiveContent = null,
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
onProgress = {},
httpAuth = account::createHTTPAuthorization,
context = context,
@@ -223,7 +223,7 @@ class ChannelMetadataViewModel : ViewModel() {
alt = null,
sensitiveContent = null,
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) },
httpAuth = account::createBlossomUploadAuth,
context = context,
)
@@ -588,7 +588,7 @@ open class ChannelNewMessageViewModel :
viewModelScope.launch(Dispatchers.IO) {
iMetaAttachments.downloadAndPrepare(
item.link.url,
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false) },
)
}
@@ -541,7 +541,7 @@ open class NewProductViewModel :
viewModelScope.launch(Dispatchers.IO) {
iMetaDescription.downloadAndPrepare(
item.link.url,
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false) },
)
}
@@ -786,7 +786,7 @@ open class ShortNotePostViewModel :
item.link.url,
) {
Amethyst.instance.okHttpClients.getHttpClient(
accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url)
accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url)
?: false,
)
}
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextFieldDefaults.contentPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -36,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
@Composable
fun MessageButton(
@@ -49,12 +51,10 @@ fun MessageButton(
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = {
accountViewModel.createChatRoomFor(user) { chatroomKey ->
nav.nav(
routeToMessage(
room = chatroomKey,
account = accountViewModel.account,
),
nav.nav {
routeToMessage(
room = ChatroomKey(setOf(user.pubkeyHex)),
account = accountViewModel.account,
)
}
},
@@ -72,7 +72,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
Nip11CachedRetriever.loadRelayInfo(
relay = item.relay,
okHttpClient = {
Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay))
Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay))
},
onInfo = {
togglePaidRelay(item, it.limitation?.payment_required ?: false)
@@ -93,7 +93,7 @@ class Nip65RelayListViewModel : ViewModel() {
_homeRelays.value.forEach { item ->
Nip11CachedRetriever.loadRelayInfo(
relay = item.relay,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) },
onInfo = {
toggleHomePaidRelay(item, it.limitation?.payment_required ?: false)
},
@@ -104,7 +104,7 @@ class Nip65RelayListViewModel : ViewModel() {
_notificationRelays.value.forEach { item ->
Nip11CachedRetriever.loadRelayInfo(
relay = item.relay,
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) },
okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) },
onInfo = {
toggleNotifPaidRelay(item, it.limitation?.payment_required ?: false)
},
@@ -92,7 +92,7 @@ class PrivateZapTests {
val privateZapRequest =
runBlocking {
LnZapRequestEvent.create(
originalNote = poll,
zappedEvent = poll,
relays = setOf("wss://relay.damus.io/"),
signer = loggedIn,
pollOption = 0,
@@ -152,7 +152,7 @@ class PrivateZapTests {
val privateZapRequest =
runBlocking {
LnZapRequestEvent.create(
originalNote = textNote,
zappedEvent = textNote,
relays = setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"),
signer = loggedIn,
pollOption = null,
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2025 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.nip03Timestamp
interface OtsResolverBuilder {
fun build(): OtsResolver
}
@@ -29,15 +29,15 @@ class VerificationStateCache {
fun verify(
event: OtsEvent,
resolverBuilder: () -> OtsResolver,
resolverBuilder: OtsResolverBuilder,
): VerificationState {
cache.put(event.id, VerificationState.Verifying)
return event.verifyState(resolverBuilder()).also { cache.put(event.id, it) }
return event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) }
}
fun cacheVerify(
event: OtsEvent,
resolverBuilder: () -> OtsResolver,
resolverBuilder: OtsResolverBuilder,
): VerificationState =
when (val verif = cache[event.id]) {
is VerificationState.Verifying -> verif
@@ -45,7 +45,7 @@ class VerificationStateCache {
is VerificationState.NetworkError -> {
// try again in 5 mins
if (verif.time < TimeUtils.fiveMinutesAgo()) {
event.verifyState(resolverBuilder()).also { cache.put(event.id, it) }
event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) }
} else {
verify(event, resolverBuilder)
}
@@ -87,8 +87,8 @@ class LnZapRequestEvent(
const val ALT = "Zap request"
suspend fun create(
originalNote: Event,
relays: Set<String>,
zappedEvent: Event,
relays: Set<NormalizedRelayUrl>,
signer: NostrSigner,
pollOption: Int?,
message: String,
@@ -98,13 +98,13 @@ class LnZapRequestEvent(
): LnZapRequestEvent {
var tags =
listOf(
arrayOf("e", originalNote.id),
arrayOf("p", toUserPubHex ?: originalNote.pubKey),
arrayOf("relays") + relays,
arrayOf("e", zappedEvent.id),
arrayOf("p", toUserPubHex ?: zappedEvent.pubKey),
arrayOf("relays") + relays.map { it.url },
AltTag.assemble(ALT),
)
if (originalNote is AddressableEvent) {
tags = tags + listOf(arrayOf("a", originalNote.aTag().toTag()))
if (zappedEvent is AddressableEvent) {
tags = tags + listOf(arrayOf("a", zappedEvent.aTag().toTag()))
}
if (pollOption != null && pollOption >= 0) {
tags = tags + listOf(arrayOf(PollOptionTag.TAG_NAME, pollOption.toString()))