mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
Merge pull request #3123 from vitorpamplona/claude/relaxed-edison-N0F4J
Support ephemeral signers for anonymous post uploads
This commit is contained in:
@@ -1888,8 +1888,8 @@ class Account(
|
||||
suspend fun <T : Event> signAnonymouslyAndBroadcast(
|
||||
template: EventTemplate<T>,
|
||||
broadcast: List<Event> = emptyList(),
|
||||
anonymousSigner: NostrSigner = NostrSignerInternal(KeyPair()),
|
||||
): T {
|
||||
val anonymousSigner = NostrSignerInternal(KeyPair())
|
||||
val event = anonymousSigner.sign(template)
|
||||
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.ciphers.NostrCipher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
@@ -66,6 +67,7 @@ class MultiOrchestrator(
|
||||
stripMetadata: Boolean = true,
|
||||
onStrippingFailed: suspend () -> Boolean = { true },
|
||||
convertGifToMp4: Boolean = false,
|
||||
forcedSigner: NostrSigner? = null,
|
||||
): Result {
|
||||
coroutineScope {
|
||||
val jobs =
|
||||
@@ -84,6 +86,7 @@ class MultiOrchestrator(
|
||||
stripMetadata,
|
||||
onStrippingFailed,
|
||||
convertGifToMp4 = convertGifToMp4,
|
||||
forcedSigner = forcedSigner,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -106,6 +109,7 @@ class MultiOrchestrator(
|
||||
stripMetadata: Boolean = true,
|
||||
onStrippingFailed: suspend () -> Boolean = { true },
|
||||
convertGifToMp4: Boolean = false,
|
||||
forcedSigner: NostrSigner? = null,
|
||||
): Result {
|
||||
coroutineScope {
|
||||
val jobs =
|
||||
@@ -125,6 +129,7 @@ class MultiOrchestrator(
|
||||
stripMetadata,
|
||||
onStrippingFailed,
|
||||
convertGifToMp4 = convertGifToMp4,
|
||||
forcedSigner = forcedSigner,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+23
-6
@@ -30,7 +30,10 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.ciphers.NostrCipher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -142,6 +145,7 @@ class UploadOrchestrator {
|
||||
contentTypeForResult: String?,
|
||||
originalHash: String?,
|
||||
account: Account,
|
||||
forcedSigner: NostrSigner?,
|
||||
context: Context,
|
||||
): UploadingFinalState {
|
||||
updateState(0.2, UploadingState.Uploading)
|
||||
@@ -158,7 +162,12 @@ class UploadOrchestrator {
|
||||
onProgress = { percent: Float ->
|
||||
updateState(0.2 + (0.2 * percent), UploadingState.Uploading)
|
||||
},
|
||||
httpAuth = account::createHTTPAuthorization,
|
||||
httpAuth =
|
||||
if (forcedSigner != null) {
|
||||
{ url, method, body -> forcedSigner.sign(HTTPAuthorizationEvent.build(url, method, body)) }
|
||||
} else {
|
||||
account::createHTTPAuthorization
|
||||
},
|
||||
context = context,
|
||||
)
|
||||
|
||||
@@ -187,6 +196,7 @@ class UploadOrchestrator {
|
||||
contentTypeForResult: String?,
|
||||
originalHash: String?,
|
||||
account: Account,
|
||||
forcedSigner: NostrSigner?,
|
||||
context: Context,
|
||||
): UploadingFinalState {
|
||||
updateState(0.2, UploadingState.Uploading)
|
||||
@@ -201,7 +211,12 @@ class UploadOrchestrator {
|
||||
sensitiveContent = contentWarningReason,
|
||||
serverBaseUrl = serverBaseUrl,
|
||||
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||
httpAuth = account::createBlossomUploadAuth,
|
||||
httpAuth =
|
||||
if (forcedSigner != null) {
|
||||
{ hash, size, alt -> BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, forcedSigner) }
|
||||
} else {
|
||||
account::createBlossomUploadAuth
|
||||
},
|
||||
context = context,
|
||||
)
|
||||
|
||||
@@ -360,6 +375,7 @@ class UploadOrchestrator {
|
||||
stripMetadata: Boolean = true,
|
||||
onStrippingFailed: suspend () -> Boolean = { true },
|
||||
convertGifToMp4: Boolean = false,
|
||||
forcedSigner: NostrSigner? = null,
|
||||
): UploadingFinalState {
|
||||
val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4)
|
||||
|
||||
@@ -379,8 +395,8 @@ class UploadOrchestrator {
|
||||
try {
|
||||
return when (server.type) {
|
||||
ServerType.NIP95 -> uploadNIP95(finalUri, compressed.contentType, null, null, context)
|
||||
ServerType.NIP96 -> uploadNIP96(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, context)
|
||||
ServerType.Blossom -> uploadBlossom(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, context)
|
||||
ServerType.NIP96 -> uploadNIP96(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, forcedSigner, context)
|
||||
ServerType.Blossom -> uploadBlossom(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, forcedSigner, context)
|
||||
}
|
||||
} finally {
|
||||
deleteTempUri(finalUri, uri)
|
||||
@@ -401,6 +417,7 @@ class UploadOrchestrator {
|
||||
stripMetadata: Boolean = true,
|
||||
onStrippingFailed: suspend () -> Boolean = { true },
|
||||
convertGifToMp4: Boolean = false,
|
||||
forcedSigner: NostrSigner? = null,
|
||||
): UploadingFinalState {
|
||||
val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4)
|
||||
|
||||
@@ -423,8 +440,8 @@ class UploadOrchestrator {
|
||||
try {
|
||||
return when (server.type) {
|
||||
ServerType.NIP95 -> uploadNIP95(encrypted.uri, encrypted.contentType, compressed.contentType, encrypted.originalHash, context)
|
||||
ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context)
|
||||
ServerType.Blossom -> uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, context)
|
||||
ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context)
|
||||
ServerType.Blossom -> uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context)
|
||||
}
|
||||
} finally {
|
||||
deleteTempUri(encrypted.uri, uri)
|
||||
|
||||
+14
-1
@@ -71,8 +71,11 @@ import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.hasGeohashes
|
||||
@@ -215,6 +218,14 @@ open class CommentPostViewModel :
|
||||
|
||||
var wantsAnonymousPost by mutableStateOf(false)
|
||||
|
||||
// A single ephemeral signer reused for the whole compose session so that media
|
||||
// uploads (Blossom/NIP-96 auth events) and the final anonymous post are all signed
|
||||
// by the same throwaway key, instead of leaking the real account's pubkey into the
|
||||
// upload authorization (and therefore into the returned media URL).
|
||||
private var anonymousSignerCache: NostrSigner? = null
|
||||
|
||||
fun anonymousSigner(): NostrSigner = anonymousSignerCache ?: NostrSignerInternal(KeyPair()).also { anonymousSignerCache = it }
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
@@ -452,7 +463,7 @@ open class CommentPostViewModel :
|
||||
cancel()
|
||||
|
||||
if (anonymous) {
|
||||
accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast)
|
||||
accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast, anonymousSigner())
|
||||
} else {
|
||||
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
|
||||
}
|
||||
@@ -619,6 +630,7 @@ open class CommentPostViewModel :
|
||||
context,
|
||||
stripMetadata = stripMetadata,
|
||||
onStrippingFailed = strippingFailureConfirmation::awaitConfirmation,
|
||||
forcedSigner = if (wantsAnonymousPost) anonymousSigner() else null,
|
||||
)
|
||||
|
||||
if (results.allGood) {
|
||||
@@ -711,6 +723,7 @@ open class CommentPostViewModel :
|
||||
wantsToAddGeoHash = false
|
||||
wantsSecretEmoji = false
|
||||
wantsAnonymousPost = false
|
||||
anonymousSignerCache = null
|
||||
|
||||
forwardZapTo.value = SplitBuilder()
|
||||
forwardZapToEditting.clearText()
|
||||
|
||||
+15
-1
@@ -91,7 +91,10 @@ import com.vitorpamplona.quartz.experimental.zapPolls.minAmount
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
|
||||
@@ -306,6 +309,14 @@ open class ShortNotePostViewModel :
|
||||
// Anonymous Reply
|
||||
var wantsAnonymousPost by mutableStateOf(false)
|
||||
|
||||
// A single ephemeral signer reused for the whole compose session so that media
|
||||
// uploads (Blossom/NIP-96 auth events) and the final anonymous post are all signed
|
||||
// by the same throwaway key, instead of leaking the real account's pubkey into the
|
||||
// upload authorization (and therefore into the returned media URL).
|
||||
private var anonymousSignerCache: NostrSigner? = null
|
||||
|
||||
fun anonymousSigner(): NostrSigner = anonymousSignerCache ?: NostrSignerInternal(KeyPair()).also { anonymousSignerCache = it }
|
||||
|
||||
// Scheduled posting: epoch seconds (UTC) when the post should be published.
|
||||
// Null = post immediately on Send (existing behavior).
|
||||
var scheduledForSec by mutableStateOf<Long?>(null)
|
||||
@@ -870,7 +881,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
if (anonymous) {
|
||||
accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast)
|
||||
accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast, anonymousSigner())
|
||||
} else if (accountViewModel.settings.useTrackedBroadcasts()) {
|
||||
// Tracked broadcasting with progress feedback (non-blocking)
|
||||
val (event, relays, extras) = accountViewModel.account.createPostEvent(template, extraNotesToBroadcast)
|
||||
@@ -1138,6 +1149,7 @@ open class ShortNotePostViewModel :
|
||||
stripMetadata,
|
||||
onStrippingFailed = strippingFailureConfirmation::awaitConfirmation,
|
||||
convertGifToMp4 = convertGifToMp4,
|
||||
forcedSigner = if (wantsAnonymousPost) anonymousSigner() else null,
|
||||
)
|
||||
|
||||
if (results.allGood) {
|
||||
@@ -1235,6 +1247,7 @@ open class ShortNotePostViewModel :
|
||||
wantsExclusiveGeoPost = false
|
||||
wantsSecretEmoji = false
|
||||
wantsAnonymousPost = false
|
||||
anonymousSignerCache = null
|
||||
scheduledForSec = null
|
||||
|
||||
forwardZapTo.value = SplitBuilder()
|
||||
@@ -1467,6 +1480,7 @@ open class ShortNotePostViewModel :
|
||||
account = account,
|
||||
context = appContext,
|
||||
useH265 = false,
|
||||
forcedSigner = if (wantsAnonymousPost) anonymousSigner() else null,
|
||||
)
|
||||
|
||||
when (result) {
|
||||
|
||||
Reference in New Issue
Block a user