Offers NostrImg as a choice of image server
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.NostrImgServer
|
||||||
|
import junit.framework.TestCase.assertNotNull
|
||||||
|
import junit.framework.TestCase.fail
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.util.Base64
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ImageUploadTesting {
|
||||||
|
|
||||||
|
val image = "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"
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun testImgurUpload() = runBlocking {
|
||||||
|
val inputStream = Base64.getDecoder().decode(image).inputStream()
|
||||||
|
|
||||||
|
println("Uploading")
|
||||||
|
|
||||||
|
ImageUploader.uploadImage(
|
||||||
|
inputStream,
|
||||||
|
"image/gif",
|
||||||
|
ImgurServer(),
|
||||||
|
onSuccess = { url, contentType ->
|
||||||
|
println("Uploaded to $url")
|
||||||
|
assertNotNull(url)
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
println("Failed to Upload")
|
||||||
|
fail("${it.message}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
@Ignore
|
||||||
|
fun testNostrBuildUpload() = runBlocking {
|
||||||
|
val inputStream = Base64.getDecoder().decode(image).inputStream()
|
||||||
|
|
||||||
|
println("Uploading")
|
||||||
|
|
||||||
|
ImageUploader.uploadImage(
|
||||||
|
inputStream,
|
||||||
|
"image/gif",
|
||||||
|
NostrBuildServer(),
|
||||||
|
onSuccess = { url, contentType ->
|
||||||
|
println("Uploaded to $url")
|
||||||
|
assertNotNull(url)
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
println("Failed to Upload")
|
||||||
|
fail("${it.message}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun testNostrImgUpload() = runBlocking {
|
||||||
|
val inputStream = Base64.getDecoder().decode(image).inputStream()
|
||||||
|
|
||||||
|
println("Uploading")
|
||||||
|
|
||||||
|
ImageUploader.uploadImage(
|
||||||
|
inputStream,
|
||||||
|
"image/gif",
|
||||||
|
NostrImgServer(),
|
||||||
|
onSuccess = { url, contentType ->
|
||||||
|
println("Uploaded to $url")
|
||||||
|
assertNotNull(url)
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
println("Failed to Upload")
|
||||||
|
fail("${it.message}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,7 +116,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
|
|||||||
LnZapEvent.kind, LnZapRequestEvent.kind,
|
LnZapEvent.kind, LnZapRequestEvent.kind,
|
||||||
ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind,
|
ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind,
|
||||||
BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind,
|
BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind,
|
||||||
PrivateDmEvent.kind, FileHeaderEvent.kind
|
PrivateDmEvent.kind,
|
||||||
|
FileHeaderEvent.kind, FileStorageEvent.kind, FileStorageHeaderEvent.kind
|
||||||
),
|
),
|
||||||
ids = interestedEvents.toList()
|
ids = interestedEvents.toList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.actions
|
|||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
@@ -9,9 +10,15 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
|
||||||
|
|
||||||
|
fun randomChars() = List(16) { charPool.random() }.joinToString("")
|
||||||
|
|
||||||
object ImageUploader {
|
object ImageUploader {
|
||||||
|
|
||||||
fun uploadImage(
|
fun uploadImage(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
server: ServersAvailable,
|
server: ServersAvailable,
|
||||||
@@ -20,9 +27,33 @@ object ImageUploader {
|
|||||||
onError: (Throwable) -> Unit
|
onError: (Throwable) -> Unit
|
||||||
) {
|
) {
|
||||||
val contentType = contentResolver.getType(uri)
|
val contentType = contentResolver.getType(uri)
|
||||||
val category = contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
val imageInputStream = contentResolver.openInputStream(uri)
|
||||||
|
|
||||||
val url = if (category == "image") "https://api.imgur.com/3/image" else "https://api.imgur.com/3/upload"
|
checkNotNull(imageInputStream) {
|
||||||
|
"Can't open the image input stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
val myServer = if (server == ServersAvailable.IMGUR) {
|
||||||
|
ImgurServer()
|
||||||
|
} else if (server == ServersAvailable.NOSTR_IMG) {
|
||||||
|
NostrImgServer()
|
||||||
|
} else {
|
||||||
|
ImgurServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadImage(imageInputStream, contentType, myServer, onSuccess, onError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadImage(
|
||||||
|
inputStream: InputStream,
|
||||||
|
contentType: String?,
|
||||||
|
server: FileServer,
|
||||||
|
onSuccess: (String, String?) -> Unit,
|
||||||
|
onError: (Throwable) -> Unit
|
||||||
|
) {
|
||||||
|
val category = contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
||||||
|
val fileName = randomChars()
|
||||||
|
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||||
|
|
||||||
val client = OkHttpClient.Builder().build()
|
val client = OkHttpClient.Builder().build()
|
||||||
|
|
||||||
@@ -30,37 +61,37 @@ object ImageUploader {
|
|||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
.addFormDataPart(
|
.addFormDataPart(
|
||||||
category,
|
category,
|
||||||
"${UUID.randomUUID()}",
|
"$fileName.$extension",
|
||||||
object : RequestBody() {
|
object : RequestBody() {
|
||||||
override fun contentType(): MediaType? =
|
override fun contentType(): MediaType? =
|
||||||
contentType?.toMediaType()
|
contentType?.toMediaType()
|
||||||
|
|
||||||
override fun writeTo(sink: BufferedSink) {
|
override fun writeTo(sink: BufferedSink) {
|
||||||
val imageInputStream = contentResolver.openInputStream(uri)
|
inputStream.source().use(sink::writeAll)
|
||||||
checkNotNull(imageInputStream) {
|
|
||||||
"Can't open the image input stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
imageInputStream.source().use(sink::writeAll)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request: Request = Request.Builder()
|
val requestBuilder = Request.Builder()
|
||||||
.header("Authorization", "Client-ID e6aea87296f3f96")
|
|
||||||
|
server.clientID()?.let {
|
||||||
|
requestBuilder.header("Authorization", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBuilder
|
||||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
.url(url)
|
.url(server.postUrl(contentType))
|
||||||
.post(requestBody)
|
.post(requestBody)
|
||||||
.build()
|
|
||||||
|
val request = requestBuilder.build()
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
try {
|
try {
|
||||||
check(response.isSuccessful)
|
check(response.isSuccessful)
|
||||||
response.body.use { body ->
|
response.body.use { body ->
|
||||||
val tree = jacksonObjectMapper().readTree(body.string())
|
val url = server.parseUrlFromSucess(body.string())
|
||||||
val url = tree?.get("data")?.get("link")?.asText()
|
|
||||||
checkNotNull(url) {
|
checkNotNull(url) {
|
||||||
"There must be an uploaded image URL in the response"
|
"There must be an uploaded image URL in the response"
|
||||||
}
|
}
|
||||||
@@ -80,3 +111,49 @@ object ImageUploader {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class FileServer {
|
||||||
|
abstract fun postUrl(contentType: String?): String
|
||||||
|
abstract fun parseUrlFromSucess(body: String): String?
|
||||||
|
|
||||||
|
open fun clientID(): String? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class NostrImgServer : FileServer() {
|
||||||
|
override fun postUrl(contentType: String?) = "https://nostrimg.com/api/upload"
|
||||||
|
|
||||||
|
override fun parseUrlFromSucess(body: String): String? {
|
||||||
|
val tree = jacksonObjectMapper().readTree(body)
|
||||||
|
val url = tree?.get("data")?.get("link")?.asText()
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clientID() = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImgurServer : FileServer() {
|
||||||
|
override fun postUrl(contentType: String?): String {
|
||||||
|
val category = contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
||||||
|
return if (category == "image") "https://api.imgur.com/3/image" else "https://api.imgur.com/3/upload"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseUrlFromSucess(body: String): String? {
|
||||||
|
val tree = jacksonObjectMapper().readTree(body)
|
||||||
|
val url = tree?.get("data")?.get("link")?.asText()
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clientID() = "Client-ID e6aea87296f3f96"
|
||||||
|
}
|
||||||
|
|
||||||
|
class NostrBuildServer : FileServer() {
|
||||||
|
override fun postUrl(contentType: String?) = "https://nostr.build/api/upload/amethyst.php"
|
||||||
|
|
||||||
|
override fun parseUrlFromSucess(body: String): String? {
|
||||||
|
val tree = jacksonObjectMapper().readTree(body)
|
||||||
|
val url = tree?.get("data")?.get("link")?.asText()
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clientID() = null
|
||||||
|
}
|
||||||
|
|||||||
@@ -488,8 +488,8 @@ fun SearchButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ServersAvailable() {
|
enum class ServersAvailable {
|
||||||
IMGUR
|
IMGUR, NOSTR_BUILD, NOSTR_IMG
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -507,7 +507,8 @@ fun ImageVideoDescription(
|
|||||||
val isVideo = mediaType.startsWith("video")
|
val isVideo = mediaType.startsWith("video")
|
||||||
|
|
||||||
val fileServers = listOf(
|
val fileServers = listOf(
|
||||||
Pair(ServersAvailable.IMGUR, "imgur.com")
|
Pair(ServersAvailable.IMGUR, "imgur.com"),
|
||||||
|
Pair(ServersAvailable.NOSTR_IMG, "nostrimg.com")
|
||||||
)
|
)
|
||||||
|
|
||||||
val fileServerOptions = fileServers.map { it.second }
|
val fileServerOptions = fileServers.map { it.second }
|
||||||
@@ -618,7 +619,7 @@ fun ImageVideoDescription(
|
|||||||
) {
|
) {
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
label = stringResource(id = R.string.file_server),
|
label = stringResource(id = R.string.file_server),
|
||||||
placeholder = fileServers.filter { it.first == defaultServer }.first().second,
|
placeholder = fileServers.filter { it.first == defaultServer }.firstOrNull()?.second ?: fileServers[0].second,
|
||||||
options = fileServerOptions,
|
options = fileServerOptions,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
selectedServer = fileServers[it].first
|
selectedServer = fileServers[it].first
|
||||||
|
|||||||
Reference in New Issue
Block a user