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 29d750d34e..8d64214427 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 @@ -42,7 +42,6 @@ fun MediaUrlContent.toCoilModel(useLocalBlossomBridge: Boolean): String = bridgeUrl( url = url, useBridge = useLocalBlossomBridge, - explicitHash = hash, mimeType = mimeType, authorPubKey = authorPubKey, skipBridge = this is MediaUrlVideo && isLiveStream, @@ -95,7 +94,6 @@ const val DEFAULT_LOCAL_CACHE_BASE = "http://127.0.0.1:24242" private fun bridgeUrl( url: String, useBridge: Boolean, - explicitHash: String?, mimeType: String?, authorPubKey: String?, skipBridge: Boolean, @@ -105,19 +103,15 @@ private fun bridgeUrl( if (!url.startsWith("http://", ignoreCase = true) && !url.startsWith("https://", ignoreCase = true)) return url // The local Blossom cache fetches `/.` on miss per BUD-01, - // which only works when the upstream URL is itself BUD-01 layout. For - // non-BUD-01 URLs (e.g. https://i.nostr.build/M5AwJ.gif) the imeta `x` - // hash identifies the blob but the upstream server doesn't host it at - // /., so trusting only `explicitHash` would point the cache - // at a 404. Require the sha to be in the URL path before bridging. - val urlSha = extractSha256FromUrlPath(url) ?: return url - - // Prefer the imeta hash when it's a valid sha256 (authoritative casing), - // otherwise fall back to what was parsed from the URL. - val sha = explicitHash?.lowercase()?.takeIf { sha256HexRegex.matches(it) } ?: urlSha + // which only works when the upstream URL is itself BUD-01 layout — the + // file at `/.` is the one named in the URL path, not the + // imeta `x` hash (which on resizing CDNs may identify a different blob + // than the URL filename, e.g. `x` = post-resize, `ox` = original). + // Always use the URL's sha; never trust the imeta override. + val sha = extractSha256FromUrlPath(url) ?: return url val ext = guessExtension(url, mimeType) - val serverBase = extractServerBase(url, urlSha) ?: return url + val serverBase = extractServerBase(url, sha) ?: return url val authors = authorPubKey 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 84dcf80c83..16e4427cac 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 @@ -255,4 +255,19 @@ class MediaUrlContentExtTest { val image = MediaUrlImage(url = url, hash = null) assertEquals(url, image.toCoilModel(useLocalBlossomBridge = true)) } + + @Test + fun bridgeOnUsesUrlShaNotImetaWhenTheyDiffer() { + // On resizing CDNs the imeta `x` (post-resize) can differ from the + // `ox` (original) embedded in the URL. The upstream file is named + // after the URL's sha, so the cache request must use that — using + // the imeta `x` would point xs= at a non-existent path on miss. + val urlSha = "f24026b7281e598973a775adefb1b9a13b9f037a94ac98dd48ccc91b83f4b7b3" + val imetaX = "6932a918de1bfae3bf6611794ff54dd677013d22b760a9212117a0bd9079badf" + val image = MediaUrlImage(url = "https://image.nostr.build/$urlSha.png", hash = imetaX) + assertEquals( + "blossom:$urlSha.png?xs=https://image.nostr.build", + image.toCoilModel(useLocalBlossomBridge = true), + ) + } }