fix(blossom): profile pictures now route through local cache
bridgeProfilePictureUrl previously returned a `blossom:` URI, but profile pictures go through ProfilePictureFetcher (Coil routes by type on `ProfilePictureUrl`), which feeds the URL straight to NetworkFetcher and never sees BlossomFetcher. NetworkFetcher then tries to issue an HTTP request against the `blossom:` scheme and fails — silently — so profile pictures from Blossom servers stopped loading. Return a direct `http://127.0.0.1:24242/<sha>.<ext>?xs=<host>&as=<pub>` URL instead. The OkHttp request goes to the local cache normally; the new redirect interceptor sees it's already on localhost and passes through unchanged.
This commit is contained in:
+48
-14
@@ -49,30 +49,49 @@ fun MediaUrlContent.toCoilModel(useLocalBlossomBridge: Boolean): String =
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bridge entry point for raw URL strings (e.g. profile pictures) where the
|
* Bridge entry point for raw URL strings (e.g. profile pictures) that go
|
||||||
* hash isn't available on a structured model. Tries to recover the sha256
|
* through a Coil fetcher routed by type (`ProfilePictureUrl`) and therefore
|
||||||
* from the URL path itself; falls back to the original URL when no hash
|
* bypass [com.vitorpamplona.quartz.nipB7Blossom.BlossomUri] processing
|
||||||
* can be determined.
|
* entirely.
|
||||||
*
|
*
|
||||||
* @param authorPubKey 64-char lowercase hex pubkey to send as `as=` so the
|
* Returns a direct `http://127.0.0.1:24242/<sha>.<ext>?xs=<host>&as=<pubkey>`
|
||||||
* local cache can consult that author's BUD-03 server list.
|
* URL so the request can flow through `NetworkFetcher` unchanged. Falls
|
||||||
|
* back to the original URL when no sha256 can be recovered from the path
|
||||||
|
* or when the bridge is off.
|
||||||
|
*
|
||||||
|
* @param localCacheBase the local Blossom cache origin (default `http://127.0.0.1:24242`).
|
||||||
|
* @param authorPubKey 64-char lowercase hex pubkey appended as `as=` so the
|
||||||
|
* cache can consult that author's BUD-03 server list on miss.
|
||||||
*/
|
*/
|
||||||
fun bridgeProfilePictureUrl(
|
fun bridgeProfilePictureUrl(
|
||||||
url: String?,
|
url: String?,
|
||||||
useBridge: Boolean,
|
useBridge: Boolean,
|
||||||
authorPubKey: String? = null,
|
authorPubKey: String? = null,
|
||||||
|
localCacheBase: String = DEFAULT_LOCAL_CACHE_BASE,
|
||||||
): String? {
|
): String? {
|
||||||
if (url == null) return null
|
if (url == null) return null
|
||||||
return bridgeUrl(
|
if (!useBridge) return url
|
||||||
url = url,
|
if (url.startsWith("blossom:", ignoreCase = true)) return url
|
||||||
useBridge = useBridge,
|
if (!url.startsWith("http://", ignoreCase = true) && !url.startsWith("https://", ignoreCase = true)) return url
|
||||||
explicitHash = null,
|
|
||||||
mimeType = null,
|
val sha = extractSha256FromUrlPath(url) ?: return url
|
||||||
authorPubKey = authorPubKey,
|
val ext = guessExtension(url, null)
|
||||||
skipBridge = false,
|
val hostBase = extractHostBase(url) ?: return url
|
||||||
)
|
|
||||||
|
val params =
|
||||||
|
buildList {
|
||||||
|
add("xs=${percentEncode(hostBase)}")
|
||||||
|
authorPubKey
|
||||||
|
?.lowercase()
|
||||||
|
?.takeIf { sha256HexRegex.matches(it) }
|
||||||
|
?.let { add("as=$it") }
|
||||||
|
}
|
||||||
|
|
||||||
|
return "${localCacheBase.removeSuffix("/")}/$sha.$ext?${params.joinToString("&")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val DEFAULT_LOCAL_CACHE_BASE = "http://127.0.0.1:24242"
|
||||||
|
|
||||||
private fun bridgeUrl(
|
private fun bridgeUrl(
|
||||||
url: String,
|
url: String,
|
||||||
useBridge: Boolean,
|
useBridge: Boolean,
|
||||||
@@ -109,6 +128,21 @@ private fun bridgeUrl(
|
|||||||
).toUriString()
|
).toUriString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun percentEncode(input: String): String {
|
||||||
|
val sb = StringBuilder(input.length)
|
||||||
|
for (c in input) {
|
||||||
|
if (c.isLetterOrDigit() || c in "-._~") {
|
||||||
|
sb.append(c)
|
||||||
|
} else {
|
||||||
|
for (b in c.toString().encodeToByteArray()) {
|
||||||
|
sb.append('%')
|
||||||
|
sb.append((b.toInt() and 0xFF).toString(16).padStart(2, '0').uppercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractSha256FromUrlPath(url: String): String? {
|
private fun extractSha256FromUrlPath(url: String): String? {
|
||||||
val pathPart = url.substringBefore('?').substringBefore('#')
|
val pathPart = url.substringBefore('?').substringBefore('#')
|
||||||
val match = sha256InPathRegex.find(pathPart) ?: return null
|
val match = sha256InPathRegex.find(pathPart) ?: return null
|
||||||
|
|||||||
+7
-1
@@ -134,7 +134,7 @@ class MediaUrlContentExtTest {
|
|||||||
val url = "https://nostr.build/i/$sha.jpg"
|
val url = "https://nostr.build/i/$sha.jpg"
|
||||||
val authorPub = "a8f3721a0dc1b4d5c12f4cc7c54ae14071eb9c1b4f9b2cf0d4ab22c0e9f0c7e5"
|
val authorPub = "a8f3721a0dc1b4d5c12f4cc7c54ae14071eb9c1b4f9b2cf0d4ab22c0e9f0c7e5"
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"blossom:$sha.jpg?xs=https://nostr.build&as=$authorPub",
|
"http://127.0.0.1:24242/$sha.jpg?xs=https%3A%2F%2Fnostr.build&as=$authorPub",
|
||||||
bridgeProfilePictureUrl(url, useBridge = true, authorPubKey = authorPub),
|
bridgeProfilePictureUrl(url, useBridge = true, authorPubKey = authorPub),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -144,4 +144,10 @@ class MediaUrlContentExtTest {
|
|||||||
val url = "https://nostr.build/avatar.jpg"
|
val url = "https://nostr.build/avatar.jpg"
|
||||||
assertEquals(url, bridgeProfilePictureUrl(url, useBridge = true))
|
assertEquals(url, bridgeProfilePictureUrl(url, useBridge = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bridgeProfilePictureUrlBlossomUriReturnedUnchanged() {
|
||||||
|
val uri = "blossom:$sha.jpg?xs=https://nostr.build"
|
||||||
|
assertEquals(uri, bridgeProfilePictureUrl(uri, useBridge = true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user