- Fixes caching bug of NIP-96 servers

- Allow NIP-96 setups with relative URLs
- Displays error messages if the server has sent in the body
- Adds a test case for both
This commit is contained in:
Vitor Pamplona 2024-08-30 18:42:41 -04:00
parent 388ccdbe75
commit aeaddf722b
7 changed files with 129 additions and 8 deletions

View File

@ -1,6 +1,30 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />

View File

@ -188,6 +188,12 @@ class ImageUploadTesting {
testBase(Nip96MediaServers.ServerName("nostpic.com", "https://nostpic.com"))
}
@Test(expected = RuntimeException::class)
fun testSprovoostNl() =
runBlocking {
testBase(Nip96MediaServers.ServerName("sprovoost.nl", "https://img.sprovoost.nl/"))
}
@Test()
@Ignore("Not Working anymore")
fun testNostrOnch() =

View File

@ -27,6 +27,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.ammolite.service.HttpClientManager
import kotlinx.coroutines.CancellationException
import okhttp3.Request
import java.net.URI
import java.net.URL
object Nip96MediaServers {
val DEFAULT =
@ -76,10 +78,26 @@ class Nip96Retriever {
val mediaTransformations: Map<MimeType, Array<String>> = emptyMap(),
)
fun parse(body: String): ServerInfo {
fun parse(
baseUrl: String,
body: String,
): ServerInfo {
val mapper =
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
return mapper.readValue(body, ServerInfo::class.java)
val serverInfo = mapper.readValue(body, ServerInfo::class.java)
return serverInfo.copy(
apiUrl = makeAbsoluteIfRelativeUrl(baseUrl, serverInfo.apiUrl),
downloadUrl = serverInfo.downloadUrl?.let { makeAbsoluteIfRelativeUrl(baseUrl, it) },
delegatedToUrl = serverInfo.delegatedToUrl?.let { makeAbsoluteIfRelativeUrl(baseUrl, it) },
tosUrl = serverInfo.tosUrl?.let { makeAbsoluteIfRelativeUrl(baseUrl, it) },
plans =
serverInfo.plans.mapValues { u ->
u.value.copy(
url = u.value.url?.let { makeAbsoluteIfRelativeUrl(baseUrl, it) },
)
},
)
}
suspend fun loadInfo(baseUrl: String): ServerInfo {
@ -98,7 +116,7 @@ class Nip96Retriever {
val body = it.body.string()
try {
if (it.isSuccessful) {
return parse(body)
return parse(baseUrl, body)
} else {
throw RuntimeException(
"Resulting Message from $baseUrl is an error: ${response.code} ${response.message}",
@ -117,3 +135,18 @@ class Nip96Retriever {
typealias PlanName = String
typealias MimeType = String
fun makeAbsoluteIfRelativeUrl(
baseUrl: String,
potentialyRelativeUrl: String,
): String =
try {
val apiUrl = URI(potentialyRelativeUrl)
if (apiUrl.isAbsolute) {
potentialyRelativeUrl
} else {
URL(URL(baseUrl), potentialyRelativeUrl).toString()
}
} catch (e: Exception) {
potentialyRelativeUrl
}

View File

@ -190,8 +190,26 @@ class Nip96Uploader(
}
}
} else {
val msg = response.body.string()
val errorMessage =
try {
val tree = jacksonObjectMapper().readTree(msg)
val status = tree.get("status")?.asText()
val message = tree.get("message")?.asText()
if (status == "error" && message != null) {
message
} else {
null
}
} catch (e: Exception) {
null
}
val explanation = HttpStatusMessages.resourceIdFor(response.code)
if (explanation != null) {
if (errorMessage != null) {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, errorMessage))
} else if (explanation != null) {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, stringRes(context, explanation)))
} else {
throw RuntimeException(stringRes(context, R.string.failed_to_upload_with_message, response.code))

View File

@ -1654,7 +1654,17 @@ fun ImageVideoDescription(
fileServers.map { TitleExplainer(it.server.name, it.server.baseUrl) }.toImmutableList()
}
var selectedServer by remember { mutableStateOf(ServerOption(defaultServer, false)) }
var selectedServer by remember {
mutableStateOf(
ServerOption(
fileServers
.firstOrNull { it.server == defaultServer }
?.server
?: fileServers[0].server,
false,
),
)
}
var message by remember { mutableStateOf("") }
var sensitiveContent by remember { mutableStateOf(false) }
@ -1778,7 +1788,7 @@ fun ImageVideoDescription(
label = stringRes(id = R.string.file_server),
placeholder =
fileServers
.firstOrNull { it.server == accountViewModel.account.settings.defaultFileServer }
.firstOrNull { it.server == defaultServer }
?.server
?.name
?: fileServers[0].server.name,

View File

@ -93,7 +93,7 @@ fun TextSpinner(
val focusRequester = remember { FocusRequester() }
val interactionSource = remember { MutableInteractionSource() }
var optionsShowing by remember { mutableStateOf(false) }
var currentText by remember { mutableStateOf(placeholder) }
var currentText by remember(placeholder) { mutableStateOf(placeholder) }
Box(
modifier = modifier,

View File

@ -24,6 +24,26 @@ import junit.framework.TestCase.assertEquals
import org.junit.Test
class Nip96Test {
val relativeUrlTest =
"""
{
"api_url": "/n96",
"download_url": "/",
"content_types": [
"image/*",
"video/*",
"audio/*"
],
"plans": {
"free": {
"name": "",
"is_nip98_required": true,
"max_byte_size": 5000000000
}
}
}
"""
val json =
"""
{
@ -116,7 +136,7 @@ class Nip96Test {
@Test()
fun parseNostrBuild() {
val info = Nip96Retriever().parse(json)
val info = Nip96Retriever().parse("https://nostr.build", json)
assertEquals("https://nostr.build/api/v2/nip96/upload", info.apiUrl)
assertEquals("https://media.nostr.build", info.downloadUrl)
@ -142,4 +162,14 @@ class Nip96Test {
assertEquals(26843545600L, info.plans["creator"]?.maxByteSize)
assertEquals(10737418240L, info.plans["professional"]?.maxByteSize)
}
@Test()
fun parseRelativeUrls() {
val info = Nip96Retriever().parse("https://test.com", relativeUrlTest)
assertEquals("https://test.com/n96", info.apiUrl)
assertEquals("https://test.com/", info.downloadUrl)
assertEquals(null, info.tosUrl)
assertEquals(null, info.delegatedToUrl)
}
}