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