mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
Merge pull request #3298 from vitorpamplona/claude/blossom-refactor-consolidate-9z9yxw
Centralize Blossom URL and auth header construction
This commit is contained in:
+6
-11
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256StreamWithCount
|
||||
@@ -52,7 +53,6 @@ import okio.ForwardingSource
|
||||
import okio.source
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.Base64
|
||||
|
||||
class BlossomUploader {
|
||||
fun Context.getFileName(uri: Uri): String? =
|
||||
@@ -120,11 +120,6 @@ class BlossomUploader {
|
||||
}.mergeLocalMetadata(localMetadata)
|
||||
}
|
||||
|
||||
fun encodeAuth(event: BlossomAuthorizationEvent): String {
|
||||
val encodedNIP98Event = Base64.getEncoder().encodeToString(event.toJson().toByteArray())
|
||||
return "Nostr $encodedNIP98Event"
|
||||
}
|
||||
|
||||
suspend fun upload(
|
||||
inputStream: InputStream,
|
||||
hash: HexKey,
|
||||
@@ -147,7 +142,7 @@ class BlossomUploader {
|
||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(it) ?: extensionFromMimeType(it)
|
||||
} ?: ""
|
||||
|
||||
val apiUrl = serverBaseUrl.removeSuffix("/") + "/upload"
|
||||
val apiUrl = BlossomServerUrl.upload(serverBaseUrl)
|
||||
|
||||
val client = okHttpClient(apiUrl)
|
||||
val requestBuilder = Request.Builder()
|
||||
@@ -187,7 +182,7 @@ class BlossomUploader {
|
||||
}
|
||||
|
||||
httpAuth(hash, length, alt?.let { "Uploading $it" } ?: "Uploading $fileName")?.let {
|
||||
requestBuilder.addHeader("Authorization", encodeAuth(it))
|
||||
requestBuilder.addHeader("Authorization", it.toAuthorizationHeader())
|
||||
}
|
||||
|
||||
contentType?.let { requestBuilder.addHeader("Content-Type", it) }
|
||||
@@ -206,7 +201,7 @@ class BlossomUploader {
|
||||
convertToMediaResult(parseResults(body.string()))
|
||||
}
|
||||
} else {
|
||||
val errorMessage = response.headers.get("X-Reason")
|
||||
val errorMessage = response.headers.get(BlossomServerUrl.REASON_HEADER)
|
||||
|
||||
val explanation = HttpStatusMessages.resourceIdFor(response.code)
|
||||
if (errorMessage != null) {
|
||||
@@ -253,12 +248,12 @@ class BlossomUploader {
|
||||
val requestBuilder = Request.Builder()
|
||||
|
||||
httpAuth(hash, "Deleting $hash")?.let {
|
||||
requestBuilder.addHeader("Authorization", encodeAuth(it))
|
||||
requestBuilder.addHeader("Authorization", it.toAuthorizationHeader())
|
||||
}
|
||||
|
||||
val request =
|
||||
requestBuilder
|
||||
.url(apiUrl.removeSuffix("/") + "/$hash.$extension")
|
||||
.url(BlossomServerUrl.blob(apiUrl, hash, extension))
|
||||
.delete()
|
||||
.build()
|
||||
|
||||
|
||||
+2
-9
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.commons.service.upload
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
|
||||
import java.util.Base64
|
||||
|
||||
object BlossomAuth {
|
||||
suspend fun createUploadAuth(
|
||||
@@ -31,13 +30,7 @@ object BlossomAuth {
|
||||
size: Long,
|
||||
alt: String,
|
||||
signer: NostrSigner,
|
||||
): String {
|
||||
val event = BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer)
|
||||
return encodeAuthHeader(event)
|
||||
}
|
||||
): String = BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer).toAuthorizationHeader()
|
||||
|
||||
fun encodeAuthHeader(event: BlossomAuthorizationEvent): String {
|
||||
val b64 = Base64.getEncoder().encodeToString(event.toJson().toByteArray())
|
||||
return "Nostr $b64"
|
||||
}
|
||||
fun encodeAuthHeader(event: BlossomAuthorizationEvent): String = event.toAuthorizationHeader()
|
||||
}
|
||||
|
||||
+5
-4
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.commons.service.upload
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -50,7 +51,7 @@ open class BlossomClient(
|
||||
authHeader: String?,
|
||||
): BlossomUploadResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val apiUrl = serverBaseUrl.removeSuffix("/") + "/upload"
|
||||
val apiUrl = BlossomServerUrl.upload(serverBaseUrl)
|
||||
val requestBody =
|
||||
object : RequestBody() {
|
||||
override fun contentType() = contentType.toMediaType()
|
||||
@@ -73,7 +74,7 @@ open class BlossomClient(
|
||||
val response = okHttpClient.newCall(requestBuilder.build()).execute()
|
||||
response.use {
|
||||
if (!it.isSuccessful) {
|
||||
val reason = it.headers["X-Reason"] ?: it.code.toString()
|
||||
val reason = it.headers[BlossomServerUrl.REASON_HEADER] ?: it.code.toString()
|
||||
throw RuntimeException("Upload failed ($serverBaseUrl): $reason")
|
||||
}
|
||||
val body = it.body.string().ifBlank { throw RuntimeException("Upload to $serverBaseUrl returned no body") }
|
||||
@@ -115,7 +116,7 @@ open class BlossomClient(
|
||||
authHeader: String?,
|
||||
): BlossomUploadResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val apiUrl = serverBaseUrl.removeSuffix("/") + "/upload"
|
||||
val apiUrl = BlossomServerUrl.upload(serverBaseUrl)
|
||||
val requestBody = bytes.toRequestBody(contentType.toMediaType())
|
||||
|
||||
val requestBuilder =
|
||||
@@ -129,7 +130,7 @@ open class BlossomClient(
|
||||
val response = okHttpClient.newCall(requestBuilder.build()).execute()
|
||||
response.use {
|
||||
if (!it.isSuccessful) {
|
||||
val reason = it.headers["X-Reason"] ?: it.code.toString()
|
||||
val reason = it.headers[BlossomServerUrl.REASON_HEADER] ?: it.code.toString()
|
||||
throw RuntimeException("Upload failed ($serverBaseUrl): $reason")
|
||||
}
|
||||
val body = it.body.string().ifBlank { throw RuntimeException("Upload to $serverBaseUrl returned no body") }
|
||||
|
||||
+15
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.io.encoding.Base64
|
||||
|
||||
@Immutable
|
||||
class BlossomAuthorizationEvent(
|
||||
@@ -35,9 +36,23 @@ class BlossomAuthorizationEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
/** Base64 of this event's JSON, as carried in the `Authorization` header value. */
|
||||
fun rawToken() = Base64.encode(toJson().encodeToByteArray())
|
||||
|
||||
/**
|
||||
* The full `Authorization` header value for a BUD-01/BUD-02 request:
|
||||
* `Nostr <base64-event>`. Mirrors NIP-98's
|
||||
* [com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent.toAuthToken],
|
||||
* which Blossom auth reuses.
|
||||
*/
|
||||
fun toAuthorizationHeader() = "$AUTH_HEADER_SCHEME${rawToken()}"
|
||||
|
||||
companion object {
|
||||
const val KIND = 24242
|
||||
|
||||
/** Scheme prefix for the `Authorization` header value (BUD-01). */
|
||||
const val AUTH_HEADER_SCHEME = "Nostr "
|
||||
|
||||
suspend fun createGetAuth(
|
||||
hash: HexKey,
|
||||
alt: String,
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.nipB7Blossom
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
/**
|
||||
* Endpoint helpers for the Blossom HTTP API (BUD-01 / BUD-02). Centralizes the
|
||||
* protocol's URL shapes and well-known header names so every transport — the
|
||||
* commons JVM `BlossomClient`, the Android uploader, the CLI — builds them the
|
||||
* same way instead of re-deriving `<server>/upload` and `X-Reason` by hand.
|
||||
*/
|
||||
object BlossomServerUrl {
|
||||
/** BUD-01 upload endpoint path: `PUT <server>/upload`. */
|
||||
const val UPLOAD_PATH = "/upload"
|
||||
|
||||
/**
|
||||
* Header a Blossom server SHOULD set with a human-readable failure reason on
|
||||
* a non-2xx response (BUD-01).
|
||||
*/
|
||||
const val REASON_HEADER = "X-Reason"
|
||||
|
||||
/** `<server>/upload`, collapsing any trailing slash on [serverBaseUrl]. */
|
||||
fun upload(serverBaseUrl: String): String = serverBaseUrl.removeSuffix("/") + UPLOAD_PATH
|
||||
|
||||
/**
|
||||
* BUD-01 blob endpoint `<server>/<sha256>[.<ext>]`, used for GET and DELETE.
|
||||
* A blank [extension] omits the suffix.
|
||||
*/
|
||||
fun blob(
|
||||
serverBaseUrl: String,
|
||||
hash: HexKey,
|
||||
extension: String = "",
|
||||
): String {
|
||||
val suffix = if (extension.isBlank()) "" else ".$extension"
|
||||
return serverBaseUrl.removeSuffix("/") + "/" + hash + suffix
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user