diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt index d0eae4ade..f4c90a25c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt @@ -49,30 +49,49 @@ fun MediaUrlContent.toCoilModel(useLocalBlossomBridge: Boolean): String = ) /** - * Bridge entry point for raw URL strings (e.g. profile pictures) where the - * hash isn't available on a structured model. Tries to recover the sha256 - * from the URL path itself; falls back to the original URL when no hash - * can be determined. + * Bridge entry point for raw URL strings (e.g. profile pictures) that go + * through a Coil fetcher routed by type (`ProfilePictureUrl`) and therefore + * bypass [com.vitorpamplona.quartz.nipB7Blossom.BlossomUri] processing + * entirely. * - * @param authorPubKey 64-char lowercase hex pubkey to send as `as=` so the - * local cache can consult that author's BUD-03 server list. + * Returns a direct `http://127.0.0.1:24242/.?xs=&as=` + * 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( url: String?, useBridge: Boolean, authorPubKey: String? = null, + localCacheBase: String = DEFAULT_LOCAL_CACHE_BASE, ): String? { if (url == null) return null - return bridgeUrl( - url = url, - useBridge = useBridge, - explicitHash = null, - mimeType = null, - authorPubKey = authorPubKey, - skipBridge = false, - ) + if (!useBridge) return url + if (url.startsWith("blossom:", ignoreCase = true)) return url + if (!url.startsWith("http://", ignoreCase = true) && !url.startsWith("https://", ignoreCase = true)) return url + + val sha = extractSha256FromUrlPath(url) ?: return url + val ext = guessExtension(url, null) + 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( url: String, useBridge: Boolean, @@ -109,6 +128,21 @@ private fun bridgeUrl( ).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? { val pathPart = url.substringBefore('?').substringBefore('#') val match = sha256InPathRegex.find(pathPart) ?: return null diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt index cbca9375e..2e3e16c73 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt @@ -134,7 +134,7 @@ class MediaUrlContentExtTest { val url = "https://nostr.build/i/$sha.jpg" val authorPub = "a8f3721a0dc1b4d5c12f4cc7c54ae14071eb9c1b4f9b2cf0d4ab22c0e9f0c7e5" 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), ) } @@ -144,4 +144,10 @@ class MediaUrlContentExtTest { val url = "https://nostr.build/avatar.jpg" assertEquals(url, bridgeProfilePictureUrl(url, useBridge = true)) } + + @Test + fun bridgeProfilePictureUrlBlossomUriReturnedUnchanged() { + val uri = "blossom:$sha.jpg?xs=https://nostr.build" + assertEquals(uri, bridgeProfilePictureUrl(uri, useBridge = true)) + } }