mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2024-09-29 16:30:49 +00:00
- Migrates the old imageservers to NIP-96 for the main feed, chats, private messages, stories and user metadata uploads.
- Fixes hash calculation of the entire payload. - Unifies uploads into NIP-94 images.
This commit is contained in:
parent
72b4f6acef
commit
b3bdbbed98
@ -2,73 +2,107 @@ package com.vitorpamplona.amethyst
|
|||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.ui.actions.FileServer
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
|
import com.vitorpamplona.amethyst.service.Nip96Retriever
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
|
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NostrFilesDevServer
|
import com.vitorpamplona.amethyst.ui.actions.Nip96Uploader
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NostrImgServer
|
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import junit.framework.TestCase.fail
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ImageUploadTesting {
|
class ImageUploadTesting {
|
||||||
|
val contentType = "image/gif"
|
||||||
val image = "R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw=="
|
val image = "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"
|
||||||
|
|
||||||
private fun testBase(server: FileServer) {
|
val contentTypePng = "image/png"
|
||||||
val bytes = Base64.getDecoder().decode(image)
|
val imagePng = "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII="
|
||||||
|
|
||||||
|
private suspend fun testBase(server: Nip96MediaServers.ServerName) {
|
||||||
|
val serverInfo = Nip96Retriever().loadInfo(
|
||||||
|
server.baseUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
val bytes = Base64.getDecoder().decode(imagePng)
|
||||||
val inputStream = bytes.inputStream()
|
val inputStream = bytes.inputStream()
|
||||||
|
|
||||||
val countDownLatch = CountDownLatch(1)
|
val result = Nip96Uploader(Account(KeyPair())).uploadImage(
|
||||||
var url: String? = null
|
|
||||||
var error: String? = null
|
|
||||||
|
|
||||||
ImageUploader(Account(KeyPair())).uploadImage(
|
|
||||||
inputStream,
|
inputStream,
|
||||||
bytes.size.toLong(),
|
bytes.size.toLong(),
|
||||||
"image/gif",
|
contentTypePng,
|
||||||
server,
|
alt = null,
|
||||||
onSuccess = { newUrl, contentType ->
|
sensitiveContent = null,
|
||||||
println("Uploaded $contentType to $url")
|
serverInfo,
|
||||||
url = newUrl
|
onProgress = {
|
||||||
countDownLatch.countDown()
|
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
println("Failed to Upload")
|
|
||||||
error = it.message
|
|
||||||
countDownLatch.countDown()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
countDownLatch.await()
|
val url = result.tags!!.first() { it[0] == "url" }.get(1)
|
||||||
|
val size = result.tags!!.firstOrNull() { it[0] == "size" }?.get(1)
|
||||||
|
val dim = result.tags!!.firstOrNull() { it[0] == "dim" }?.get(1)
|
||||||
|
val hash = result.tags!!.firstOrNull() { it[0] == "x" }?.get(1)
|
||||||
|
|
||||||
Assert.assertNull(error)
|
Assert.assertTrue(url.startsWith("http"))
|
||||||
Assert.assertTrue(url?.startsWith("http") == true)
|
|
||||||
|
val imageData: ByteArray = ImageDownloader().waitAndGetImage(url) ?: run {
|
||||||
|
fail("Should not be null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
FileHeader.prepare(
|
||||||
|
imageData,
|
||||||
|
contentTypePng,
|
||||||
|
null,
|
||||||
|
onReady = {
|
||||||
|
if (dim != null) {
|
||||||
|
assertEquals(dim, it.dim)
|
||||||
|
}
|
||||||
|
if (size != null) {
|
||||||
|
assertEquals(size, it.size.toString())
|
||||||
|
}
|
||||||
|
if (hash != null) {
|
||||||
|
assertEquals(hash, it.hash)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
fail("It should not fail")
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
fun testImgurUpload() = runBlocking {
|
fun testNostrCheck() = runBlocking {
|
||||||
testBase(ImgurServer())
|
testBase(Nip96MediaServers.ServerName("nostrcheck.me", "https://nostrcheck.me"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
fun testNostrBuildUpload() = runBlocking {
|
fun testNostrage() = runBlocking {
|
||||||
testBase(NostrBuildServer())
|
testBase(Nip96MediaServers.ServerName("nostrage", "https://nostrage.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
fun testNostrImgUpload() = runBlocking {
|
fun testSove() = runBlocking {
|
||||||
testBase(NostrImgServer())
|
testBase(Nip96MediaServers.ServerName("sove", "https://sove.rent"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test()
|
||||||
fun testNostrFilesDevUpload() = runBlocking {
|
fun testNostrBuild() = runBlocking {
|
||||||
testBase(NostrFilesDevServer())
|
testBase(Nip96MediaServers.ServerName("nostr.build", "https://nostr.build"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun testSovbit() = runBlocking {
|
||||||
|
testBase(Nip96MediaServers.ServerName("sovbit", "https://files.sovbit.host"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun testVoidCat() = runBlocking {
|
||||||
|
testBase(Nip96MediaServers.ServerName("void.cat", "https://void.cat"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,13 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
|||||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.Nip47URI
|
import com.vitorpamplona.amethyst.model.Nip47URI
|
||||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
|
||||||
import com.vitorpamplona.amethyst.model.Settings
|
import com.vitorpamplona.amethyst.model.Settings
|
||||||
import com.vitorpamplona.amethyst.model.ThemeType
|
import com.vitorpamplona.amethyst.model.ThemeType
|
||||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||||
import com.vitorpamplona.amethyst.model.parseThemeType
|
import com.vitorpamplona.amethyst.model.parseThemeType
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClient
|
||||||
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||||
@ -264,7 +264,7 @@ object LocalPreferences {
|
|||||||
putString(PrefKeys.ZAP_AMOUNTS, Event.mapper.writeValueAsString(account.zapAmountChoices))
|
putString(PrefKeys.ZAP_AMOUNTS, Event.mapper.writeValueAsString(account.zapAmountChoices))
|
||||||
putString(PrefKeys.REACTION_CHOICES, Event.mapper.writeValueAsString(account.reactionChoices))
|
putString(PrefKeys.REACTION_CHOICES, Event.mapper.writeValueAsString(account.reactionChoices))
|
||||||
putString(PrefKeys.DEFAULT_ZAPTYPE, account.defaultZapType.name)
|
putString(PrefKeys.DEFAULT_ZAPTYPE, account.defaultZapType.name)
|
||||||
putString(PrefKeys.DEFAULT_FILE_SERVER, account.defaultFileServer.name)
|
putString(PrefKeys.DEFAULT_FILE_SERVER, Event.mapper.writeValueAsString(account.defaultFileServer))
|
||||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList.value)
|
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList.value)
|
||||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList.value)
|
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList.value)
|
||||||
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList.value)
|
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList.value)
|
||||||
@ -427,9 +427,15 @@ object LocalPreferences {
|
|||||||
LnZapEvent.ZapType.values().firstOrNull() { it.name == serverName }
|
LnZapEvent.ZapType.values().firstOrNull() { it.name == serverName }
|
||||||
} ?: LnZapEvent.ZapType.PUBLIC
|
} ?: LnZapEvent.ZapType.PUBLIC
|
||||||
|
|
||||||
val defaultFileServer = getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
|
val defaultFileServer = try {
|
||||||
ServersAvailable.values().firstOrNull() { it.name == serverName }
|
getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
|
||||||
} ?: ServersAvailable.NOSTR_BUILD
|
Event.mapper.readValue<Nip96MediaServers.ServerName>(serverName)
|
||||||
|
} ?: Nip96MediaServers.DEFAULT[0]
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("LocalPreferences", "Failed to decode saved File Server", e)
|
||||||
|
e.printStackTrace()
|
||||||
|
Nip96MediaServers.DEFAULT[0]
|
||||||
|
}
|
||||||
|
|
||||||
val zapPaymentRequestServer = try {
|
val zapPaymentRequestServer = try {
|
||||||
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
||||||
|
@ -4,8 +4,6 @@ import android.content.res.Resources
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
@ -15,6 +13,7 @@ import androidx.lifecycle.switchMap
|
|||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.OptOutFromFilters
|
import com.vitorpamplona.amethyst.OptOutFromFilters
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
@ -132,7 +131,7 @@ class Account(
|
|||||||
var zapAmountChoices: List<Long> = DefaultZapAmounts,
|
var zapAmountChoices: List<Long> = DefaultZapAmounts,
|
||||||
var reactionChoices: List<String> = DefaultReactions,
|
var reactionChoices: List<String> = DefaultReactions,
|
||||||
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
|
var defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PRIVATE,
|
||||||
var defaultFileServer: ServersAvailable = ServersAvailable.NOSTR_BUILD,
|
var defaultFileServer: Nip96MediaServers.ServerName = Nip96MediaServers.DEFAULT[0],
|
||||||
var defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS),
|
var defaultHomeFollowList: MutableStateFlow<String> = MutableStateFlow(KIND3_FOLLOWS),
|
||||||
var defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
var defaultStoriesFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||||
var defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
var defaultNotificationFollowList: MutableStateFlow<String> = MutableStateFlow(GLOBAL_FOLLOWS),
|
||||||
@ -711,7 +710,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createHTTPAuthorization(url: String, method: String, body: String? = null, onReady: (HTTPAuthorizationEvent) -> Unit) {
|
fun createHTTPAuthorization(url: String, method: String, body: ByteArray? = null, onReady: (HTTPAuthorizationEvent) -> Unit) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
HTTPAuthorizationEvent.create(url, method, body, signer, onReady = onReady)
|
HTTPAuthorizationEvent.create(url, method, body, signer, onReady = onReady)
|
||||||
@ -967,7 +966,13 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNip95(byteArray: ByteArray, headerInfo: FileHeader, onReady: (Pair<FileStorageEvent, FileStorageHeaderEvent>) -> Unit) {
|
fun createNip95(
|
||||||
|
byteArray: ByteArray,
|
||||||
|
headerInfo: FileHeader,
|
||||||
|
alt: String?,
|
||||||
|
sensitiveContent: Boolean,
|
||||||
|
onReady: (Pair<FileStorageEvent, FileStorageHeaderEvent>) -> Unit
|
||||||
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
FileStorageEvent.create(
|
FileStorageEvent.create(
|
||||||
@ -982,8 +987,8 @@ class Account(
|
|||||||
size = headerInfo.size.toString(),
|
size = headerInfo.size.toString(),
|
||||||
dimensions = headerInfo.dim,
|
dimensions = headerInfo.dim,
|
||||||
blurhash = headerInfo.blurHash,
|
blurhash = headerInfo.blurHash,
|
||||||
alt = headerInfo.alt,
|
alt = alt,
|
||||||
sensitiveContent = headerInfo.sensitiveContent,
|
sensitiveContent = sensitiveContent,
|
||||||
signer = signer
|
signer = signer
|
||||||
) { signedEvent ->
|
) { signedEvent ->
|
||||||
onReady(
|
onReady(
|
||||||
@ -993,7 +998,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendNip95(data: FileStorageEvent, signedEvent: FileStorageHeaderEvent, relayList: List<Relay>? = null): Note? {
|
fun consumeAndSendNip95(data: FileStorageEvent, signedEvent: FileStorageHeaderEvent, relayList: List<Relay>? = null): Note? {
|
||||||
if (!isWriteable()) return null
|
if (!isWriteable()) return null
|
||||||
|
|
||||||
Client.send(data, relayList = relayList)
|
Client.send(data, relayList = relayList)
|
||||||
@ -1005,7 +1010,19 @@ class Account(
|
|||||||
return LocalCache.notes[signedEvent.id]
|
return LocalCache.notes[signedEvent.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendHeader(signedEvent: FileHeaderEvent, relayList: List<Relay>? = null, onReady: (Note) -> Unit) {
|
fun consumeNip95(data: FileStorageEvent, signedEvent: FileStorageHeaderEvent): Note? {
|
||||||
|
LocalCache.consume(data, null)
|
||||||
|
LocalCache.consume(signedEvent, null)
|
||||||
|
|
||||||
|
return LocalCache.notes[signedEvent.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendNip95(data: FileStorageEvent, signedEvent: FileStorageHeaderEvent, relayList: List<Relay>? = null) {
|
||||||
|
Client.send(data, relayList = relayList)
|
||||||
|
Client.send(signedEvent, relayList = relayList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendHeader(signedEvent: FileHeaderEvent, relayList: List<Relay>? = null, onReady: (Note) -> Unit) {
|
||||||
Client.send(signedEvent, relayList = relayList)
|
Client.send(signedEvent, relayList = relayList)
|
||||||
LocalCache.consume(signedEvent, null)
|
LocalCache.consume(signedEvent, null)
|
||||||
|
|
||||||
@ -1014,18 +1031,57 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendHeader(headerInfo: FileHeader, relayList: List<Relay>? = null, onReady: (Note) -> Unit) {
|
fun createHeader(
|
||||||
|
imageUrl: String,
|
||||||
|
magnetUri: String?,
|
||||||
|
headerInfo: FileHeader,
|
||||||
|
alt: String?,
|
||||||
|
sensitiveContent: Boolean,
|
||||||
|
originalHash: String? = null,
|
||||||
|
onReady: (FileHeaderEvent) -> Unit
|
||||||
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
FileHeaderEvent.create(
|
FileHeaderEvent.create(
|
||||||
url = headerInfo.url,
|
url = imageUrl,
|
||||||
|
magnetUri = magnetUri,
|
||||||
mimeType = headerInfo.mimeType,
|
mimeType = headerInfo.mimeType,
|
||||||
hash = headerInfo.hash,
|
hash = headerInfo.hash,
|
||||||
size = headerInfo.size.toString(),
|
size = headerInfo.size.toString(),
|
||||||
dimensions = headerInfo.dim,
|
dimensions = headerInfo.dim,
|
||||||
blurhash = headerInfo.blurHash,
|
blurhash = headerInfo.blurHash,
|
||||||
alt = headerInfo.alt,
|
alt = alt,
|
||||||
sensitiveContent = headerInfo.sensitiveContent,
|
originalHash = originalHash,
|
||||||
|
sensitiveContent = sensitiveContent,
|
||||||
|
signer = signer
|
||||||
|
) { event ->
|
||||||
|
onReady(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendHeader(
|
||||||
|
imageUrl: String,
|
||||||
|
magnetUri: String?,
|
||||||
|
headerInfo: FileHeader,
|
||||||
|
alt: String?,
|
||||||
|
sensitiveContent: Boolean,
|
||||||
|
originalHash: String? = null,
|
||||||
|
relayList: List<Relay>? = null,
|
||||||
|
onReady: (Note) -> Unit
|
||||||
|
) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
FileHeaderEvent.create(
|
||||||
|
url = imageUrl,
|
||||||
|
magnetUri = magnetUri,
|
||||||
|
mimeType = headerInfo.mimeType,
|
||||||
|
hash = headerInfo.hash,
|
||||||
|
size = headerInfo.size.toString(),
|
||||||
|
dimensions = headerInfo.dim,
|
||||||
|
blurhash = headerInfo.blurHash,
|
||||||
|
alt = alt,
|
||||||
|
originalHash = originalHash,
|
||||||
|
sensitiveContent = sensitiveContent,
|
||||||
signer = signer
|
signer = signer
|
||||||
) { event ->
|
) { event ->
|
||||||
sendHeader(event, relayList = relayList, onReady)
|
sendHeader(event, relayList = relayList, onReady)
|
||||||
@ -1046,7 +1102,8 @@ class Account(
|
|||||||
wantsToMarkAsSensitive: Boolean,
|
wantsToMarkAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
relayList: List<Relay>? = null,
|
relayList: List<Relay>? = null,
|
||||||
geohash: String? = null
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<Event>? = null
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@ -1072,6 +1129,7 @@ class Account(
|
|||||||
zapRaiserAmount = zapRaiserAmount,
|
zapRaiserAmount = zapRaiserAmount,
|
||||||
directMentions = directMentions,
|
directMentions = directMentions,
|
||||||
geohash = geohash,
|
geohash = geohash,
|
||||||
|
nip94attachments = nip94attachments,
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it, relayList = relayList)
|
Client.send(it, relayList = relayList)
|
||||||
@ -1102,7 +1160,8 @@ class Account(
|
|||||||
root: String?,
|
root: String?,
|
||||||
directMentions: Set<HexKey>,
|
directMentions: Set<HexKey>,
|
||||||
relayList: List<Relay>? = null,
|
relayList: List<Relay>? = null,
|
||||||
geohash: String? = null
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<Event>? = null
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@ -1123,6 +1182,7 @@ class Account(
|
|||||||
root = root,
|
root = root,
|
||||||
directMentions = directMentions,
|
directMentions = directMentions,
|
||||||
geohash = geohash,
|
geohash = geohash,
|
||||||
|
nip94attachments = nip94attachments,
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it, relayList = relayList)
|
Client.send(it, relayList = relayList)
|
||||||
@ -1160,7 +1220,8 @@ class Account(
|
|||||||
wantsToMarkAsSensitive: Boolean,
|
wantsToMarkAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long? = null,
|
zapRaiserAmount: Long? = null,
|
||||||
relayList: List<Relay>? = null,
|
relayList: List<Relay>? = null,
|
||||||
geohash: String? = null
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<Event>? = null
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
@ -1182,7 +1243,8 @@ class Account(
|
|||||||
zapReceiver = zapReceiver,
|
zapReceiver = zapReceiver,
|
||||||
markAsSensitive = wantsToMarkAsSensitive,
|
markAsSensitive = wantsToMarkAsSensitive,
|
||||||
zapRaiserAmount = zapRaiserAmount,
|
zapRaiserAmount = zapRaiserAmount,
|
||||||
geohash = geohash
|
geohash = geohash,
|
||||||
|
nip94attachments = nip94attachments
|
||||||
) {
|
) {
|
||||||
Client.send(it, relayList = relayList)
|
Client.send(it, relayList = relayList)
|
||||||
LocalCache.justConsume(it, null)
|
LocalCache.justConsume(it, null)
|
||||||
@ -1201,7 +1263,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: List<ZapSplitSetup>? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
|
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: List<ZapSplitSetup>? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null, nip94attachments: List<Event>? = null) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
val repliesToHex = replyTo?.map { it.idHex }
|
val repliesToHex = replyTo?.map { it.idHex }
|
||||||
@ -1216,6 +1278,7 @@ class Account(
|
|||||||
markAsSensitive = wantsToMarkAsSensitive,
|
markAsSensitive = wantsToMarkAsSensitive,
|
||||||
zapRaiserAmount = zapRaiserAmount,
|
zapRaiserAmount = zapRaiserAmount,
|
||||||
geohash = geohash,
|
geohash = geohash,
|
||||||
|
nip94attachments = nip94attachments,
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
@ -1223,7 +1286,7 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: List<ZapSplitSetup>? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
|
fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: List<ZapSplitSetup>? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null, nip94attachments: List<Event>? = null) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
||||||
@ -1239,6 +1302,7 @@ class Account(
|
|||||||
markAsSensitive = wantsToMarkAsSensitive,
|
markAsSensitive = wantsToMarkAsSensitive,
|
||||||
zapRaiserAmount = zapRaiserAmount,
|
zapRaiserAmount = zapRaiserAmount,
|
||||||
geohash = geohash,
|
geohash = geohash,
|
||||||
|
nip94attachments = nip94attachments,
|
||||||
signer = signer
|
signer = signer
|
||||||
) {
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
@ -1674,7 +1738,7 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeDefaultFileServer(server: ServersAvailable) {
|
fun changeDefaultFileServer(server: Nip96MediaServers.ServerName) {
|
||||||
defaultFileServer = server
|
defaultFileServer = server
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
package com.vitorpamplona.amethyst.model
|
|
||||||
|
|
||||||
enum class ServersAvailable {
|
|
||||||
// IMGUR,
|
|
||||||
NOSTR_BUILD,
|
|
||||||
NOSTRIMG,
|
|
||||||
NOSTRFILES_DEV,
|
|
||||||
NOSTRCHECK_ME,
|
|
||||||
|
|
||||||
// IMGUR_NIP_94,
|
|
||||||
NOSTRIMG_NIP_94,
|
|
||||||
NOSTR_BUILD_NIP_94,
|
|
||||||
NOSTRFILES_DEV_NIP_94,
|
|
||||||
NOSTRCHECK_ME_NIP_94,
|
|
||||||
NIP95
|
|
||||||
}
|
|
@ -10,22 +10,25 @@ import io.trbl.blurhash.BlurHash
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class FileHeader(
|
class FileHeader(
|
||||||
val url: String,
|
|
||||||
val mimeType: String?,
|
val mimeType: String?,
|
||||||
val hash: String,
|
val hash: String,
|
||||||
val size: Int,
|
val size: Int,
|
||||||
val dim: String?,
|
val dim: String?,
|
||||||
val blurHash: String?,
|
val blurHash: String?
|
||||||
val alt: String? = null,
|
|
||||||
val sensitiveContent: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun prepare(fileUrl: String, mimeType: String?, alt: String?, sensitiveContent: Boolean, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
|
suspend fun prepare(
|
||||||
|
fileUrl: String,
|
||||||
|
mimeType: String?,
|
||||||
|
dimPrecomputed: String?,
|
||||||
|
onReady: (FileHeader) -> Unit,
|
||||||
|
onError: () -> Unit
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
||||||
|
|
||||||
if (imageData != null) {
|
if (imageData != null) {
|
||||||
prepare(imageData, fileUrl, mimeType, alt, sensitiveContent, onReady, onError)
|
prepare(imageData, mimeType, dimPrecomputed, onReady, onError)
|
||||||
} else {
|
} else {
|
||||||
onError()
|
onError()
|
||||||
}
|
}
|
||||||
@ -37,10 +40,8 @@ class FileHeader(
|
|||||||
|
|
||||||
fun prepare(
|
fun prepare(
|
||||||
data: ByteArray,
|
data: ByteArray,
|
||||||
fileUrl: String,
|
|
||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
alt: String?,
|
dimPrecomputed: String?,
|
||||||
sensitiveContent: Boolean,
|
|
||||||
onReady: (FileHeader) -> Unit,
|
onReady: (FileHeader) -> Unit,
|
||||||
onError: () -> Unit
|
onError: () -> Unit
|
||||||
) {
|
) {
|
||||||
@ -76,10 +77,10 @@ class FileHeader(
|
|||||||
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4), dim)
|
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4), dim)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Pair(null, null)
|
Pair(null, dimPrecomputed)
|
||||||
}
|
}
|
||||||
|
|
||||||
onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, alt, sensitiveContent))
|
onReady(FileHeader(mimeType, hash, size, dim, blurHash))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||||
onError()
|
onError()
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import okhttp3.Request
|
||||||
|
|
||||||
|
object Nip96MediaServers {
|
||||||
|
val DEFAULT =
|
||||||
|
listOf(
|
||||||
|
ServerName("Nostr.Build", "https://nostr.build"),
|
||||||
|
ServerName("NostrCheck.me", "https://nostrcheck.me"),
|
||||||
|
ServerName("Nostrage", "https://nostrage.com"),
|
||||||
|
ServerName("Sove", "https://sove.rent"),
|
||||||
|
ServerName("Sovbit", "https://files.sovbit.host"),
|
||||||
|
ServerName("Void.cat", "https://void.cat")
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ServerName(val name: String, val baseUrl: String)
|
||||||
|
|
||||||
|
val cache: MutableMap<String, Nip96Retriever.ServerInfo> = mutableMapOf()
|
||||||
|
|
||||||
|
suspend fun load(url: String): Nip96Retriever.ServerInfo {
|
||||||
|
val cached = cache[url]
|
||||||
|
if (cached != null) return cached
|
||||||
|
|
||||||
|
val fetched = Nip96Retriever().loadInfo(url)
|
||||||
|
cache[url] = fetched
|
||||||
|
return fetched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Nip96Retriever {
|
||||||
|
data class ServerInfo(
|
||||||
|
@JsonProperty("api_url") val apiUrl: String,
|
||||||
|
@JsonProperty("download_url") val downloadUrl: String? = null,
|
||||||
|
@JsonProperty("delegated_to_url") val delegatedToUrl: String? = null,
|
||||||
|
@JsonProperty("supported_nips") val supportedNips: ArrayList<Int> = arrayListOf(),
|
||||||
|
@JsonProperty("tos_url") val tosUrl: String? = null,
|
||||||
|
@JsonProperty("content_types") val contentTypes: ArrayList<MimeType> = arrayListOf(),
|
||||||
|
@JsonProperty("plans") val plans: Map<PlanName, Plan> = mapOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Plan(
|
||||||
|
@JsonProperty("name") val name: String? = null,
|
||||||
|
@JsonProperty("is_nip98_required") val isNip98Required: Boolean? = null,
|
||||||
|
@JsonProperty("url") val url: String? = null,
|
||||||
|
@JsonProperty("max_byte_size") val maxByteSize: Long? = null,
|
||||||
|
@JsonProperty("file_expiration") val fileExpiration: ArrayList<Int> = arrayListOf(),
|
||||||
|
@JsonProperty("media_transformations") val mediaTransformations: Map<MimeType, Array<String>> = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parse(body: String): ServerInfo {
|
||||||
|
val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
return mapper.readValue(body, ServerInfo::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadInfo(
|
||||||
|
baseUrl: String
|
||||||
|
): ServerInfo {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
val request: Request = Request
|
||||||
|
.Builder()
|
||||||
|
.header("Accept", "application/nostr+json")
|
||||||
|
.url(baseUrl.removeSuffix("/") + "/.well-known/nostr/nip96.json")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
HttpClient.getHttpClient()
|
||||||
|
.newCall(request)
|
||||||
|
.execute().use { response ->
|
||||||
|
checkNotInMainThread()
|
||||||
|
response.use {
|
||||||
|
val body = it.body.string()
|
||||||
|
try {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
return parse(body)
|
||||||
|
} else {
|
||||||
|
throw RuntimeException("Resulting Message from $baseUrl is an error: ${response.code} ${response.message}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RelayInfoFail", "Resulting Message from $baseUrl in not parseable: $body", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias PlanName = String
|
||||||
|
typealias MimeType = String
|
@ -1,287 +0,0 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.actions
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import androidx.core.net.toFile
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
|
||||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
|
||||||
import com.vitorpamplona.amethyst.service.HttpClient
|
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Callback
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.MultipartBody
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import okio.BufferedSink
|
|
||||||
import okio.IOException
|
|
||||||
import okio.source
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.time.Duration
|
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
|
|
||||||
|
|
||||||
fun randomChars() = List(16) { charPool.random() }.joinToString("")
|
|
||||||
class ImageUploader(val account: Account?) {
|
|
||||||
fun uploadImage(
|
|
||||||
uri: Uri,
|
|
||||||
contentType: String?,
|
|
||||||
size: Long?,
|
|
||||||
server: ServersAvailable,
|
|
||||||
contentResolver: ContentResolver,
|
|
||||||
onSuccess: (String, String?) -> Unit,
|
|
||||||
onError: (Throwable) -> Unit
|
|
||||||
) {
|
|
||||||
val myContentType = contentType ?: contentResolver.getType(uri)
|
|
||||||
val imageInputStream = contentResolver.openInputStream(uri)
|
|
||||||
|
|
||||||
val length = size ?: contentResolver.query(uri, null, null, null, null)?.use {
|
|
||||||
it.moveToFirst()
|
|
||||||
val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
|
|
||||||
it.getLong(sizeIndex)
|
|
||||||
} ?: kotlin.runCatching {
|
|
||||||
uri.toFile().length()
|
|
||||||
}.getOrNull() ?: 0
|
|
||||||
|
|
||||||
checkNotNull(imageInputStream) {
|
|
||||||
"Can't open the image input stream"
|
|
||||||
}
|
|
||||||
val myServer = when (server) {
|
|
||||||
// ServersAvailable.IMGUR, ServersAvailable.IMGUR_NIP_94 -> {
|
|
||||||
// ImgurServer()
|
|
||||||
// }
|
|
||||||
ServersAvailable.NOSTRIMG, ServersAvailable.NOSTRIMG_NIP_94 -> {
|
|
||||||
NostrImgServer()
|
|
||||||
}
|
|
||||||
ServersAvailable.NOSTR_BUILD, ServersAvailable.NOSTR_BUILD_NIP_94 -> {
|
|
||||||
NostrBuildServer()
|
|
||||||
}
|
|
||||||
ServersAvailable.NOSTRFILES_DEV, ServersAvailable.NOSTRFILES_DEV_NIP_94 -> {
|
|
||||||
NostrFilesDevServer()
|
|
||||||
}
|
|
||||||
ServersAvailable.NOSTRCHECK_ME, ServersAvailable.NOSTRCHECK_ME_NIP_94 -> {
|
|
||||||
NostrCheckMeServer()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
NostrBuildServer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadImage(imageInputStream, length, myContentType, myServer, onSuccess, onError)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uploadImage(
|
|
||||||
inputStream: InputStream,
|
|
||||||
length: Long,
|
|
||||||
contentType: String?,
|
|
||||||
server: FileServer,
|
|
||||||
onSuccess: (String, String?) -> Unit,
|
|
||||||
onError: (Throwable) -> Unit
|
|
||||||
) {
|
|
||||||
val fileName = randomChars()
|
|
||||||
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
|
||||||
|
|
||||||
val client = HttpClient.getHttpClient(Duration.ofSeconds(30L))
|
|
||||||
val requestBody: RequestBody
|
|
||||||
val requestBuilder = Request.Builder()
|
|
||||||
|
|
||||||
requestBody = MultipartBody.Builder()
|
|
||||||
.setType(MultipartBody.FORM)
|
|
||||||
.addFormDataPart(
|
|
||||||
server.inputParameterName(contentType),
|
|
||||||
"$fileName.$extension",
|
|
||||||
|
|
||||||
object : RequestBody() {
|
|
||||||
override fun contentType() = contentType?.toMediaType()
|
|
||||||
|
|
||||||
override fun contentLength() = length
|
|
||||||
|
|
||||||
override fun writeTo(sink: BufferedSink) {
|
|
||||||
inputStream.source().use(sink::writeAll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
server.authorizationToken(account, requestBody.toString()) { authorizationToken ->
|
|
||||||
if (authorizationToken != null) {
|
|
||||||
requestBuilder.addHeader("Authorization", authorizationToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestBuilder
|
|
||||||
.addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
|
||||||
.url(server.postUrl(contentType))
|
|
||||||
.post(requestBody)
|
|
||||||
val request = requestBuilder.build()
|
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
try {
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
response.body.use { body ->
|
|
||||||
val url = server.parseUrlFromSuccess(body.string(), authorizationToken)
|
|
||||||
checkNotNull(url) {
|
|
||||||
"There must be an uploaded image URL in the response"
|
|
||||||
}
|
|
||||||
|
|
||||||
onSuccess(url, contentType)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onError(RuntimeException("Error Uploading image: ${response.code}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
onError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
onError(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NIP98Header(url: String, method: String, body: String, onReady: (String?) -> Unit) {
|
|
||||||
val myAccount = account
|
|
||||||
|
|
||||||
if (myAccount == null) {
|
|
||||||
onReady(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
myAccount.createHTTPAuthorization(url, method, body) {
|
|
||||||
val noteJson = it.toJson()
|
|
||||||
val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray())
|
|
||||||
onReady("Nostr $encodedNIP98Event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class FileServer {
|
|
||||||
abstract fun postUrl(contentType: String?): String
|
|
||||||
abstract fun parseUrlFromSuccess(body: String, authorizationToken: String?): String?
|
|
||||||
abstract fun inputParameterName(contentType: String?): String
|
|
||||||
|
|
||||||
open fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) {
|
|
||||||
onReady(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NostrImgServer : FileServer() {
|
|
||||||
override fun postUrl(contentType: String?) = "https://nostrimg.com/api/upload"
|
|
||||||
|
|
||||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
|
||||||
val tree = jacksonObjectMapper().readTree(body)
|
|
||||||
val url = tree?.get("data")?.get("link")?.asText()
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inputParameterName(contentType: String?): String {
|
|
||||||
return contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
|
||||||
val tree = jacksonObjectMapper().readTree(body)
|
|
||||||
val url = tree?.get("data")?.get("link")?.asText()
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inputParameterName(contentType: String?): String {
|
|
||||||
return contentType?.toMediaType()?.toString()?.split("/")?.get(0) ?: "image"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) {
|
|
||||||
onReady("Client-ID e6aea87296f3f96")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NostrBuildServer : FileServer() {
|
|
||||||
override fun postUrl(contentType: String?) = "https://nostr.build/api/v2/upload/files"
|
|
||||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
|
||||||
val tree = jacksonObjectMapper().readTree(body)
|
|
||||||
val data = tree?.get("data")
|
|
||||||
val data0 = data?.get(0)
|
|
||||||
val url = data0?.get("url")
|
|
||||||
return url.toString().replace("\"", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inputParameterName(contentType: String?): String {
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun authorizationToken(account: Account?, info: String, onReady: (String?) -> Unit) {
|
|
||||||
ImageUploader(account).NIP98Header("https://nostr.build/api/v2/upload/files", "POST", info, onReady)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NostrFilesDevServer : FileServer() {
|
|
||||||
override fun postUrl(contentType: String?) = "https://nostrfiles.dev/upload_image"
|
|
||||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
|
||||||
val tree = jacksonObjectMapper().readTree(body)
|
|
||||||
return tree?.get("url")?.asText()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inputParameterName(contentType: String?): String {
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NostrCheckMeServer : FileServer() {
|
|
||||||
override fun postUrl(contentType: String?) = "https://nostrcheck.me/api/v1/media"
|
|
||||||
override fun parseUrlFromSuccess(body: String, authorizationToken: String?): String? {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
val tree = jacksonObjectMapper().readTree(body)
|
|
||||||
val url = tree?.get("url")?.asText()
|
|
||||||
val id = tree?.get("id")?.asText()
|
|
||||||
var isCompleted = false
|
|
||||||
|
|
||||||
val client = HttpClient.getHttpClient()
|
|
||||||
val requrl =
|
|
||||||
"https://nostrcheck.me/api/v1/media?id=$id" // + "&apikey=26d075787d261660682fb9d20dbffa538c708b1eda921d0efa2be95fbef4910a"
|
|
||||||
|
|
||||||
val requestBuilder = Request.Builder().url(requrl)
|
|
||||||
|
|
||||||
if (authorizationToken != null) {
|
|
||||||
requestBuilder.addHeader("Authorization", authorizationToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = requestBuilder.get().build()
|
|
||||||
|
|
||||||
while (!isCompleted) {
|
|
||||||
client.newCall(request).execute().use {
|
|
||||||
val tree = jacksonObjectMapper().readTree(it.body.string())
|
|
||||||
isCompleted = tree?.get("status")?.asText() == "completed"
|
|
||||||
try {
|
|
||||||
Thread.sleep(500)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inputParameterName(contentType: String?): String {
|
|
||||||
return "mediafile"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun authorizationToken(account: Account?, body: String, onReady: (String?) -> Unit) {
|
|
||||||
ImageUploader(account).NIP98Header("https://nostrcheck.me/api/v1/media", "POST", body, onReady)
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,8 +10,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -19,6 +19,11 @@ import kotlinx.coroutines.channels.BufferOverflow
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class ServerOption(
|
||||||
|
val server: Nip96MediaServers.ServerName,
|
||||||
|
val isNip95: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
open class NewMediaModel : ViewModel() {
|
open class NewMediaModel : ViewModel() {
|
||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
@ -27,7 +32,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
var mediaType by mutableStateOf<String?>(null)
|
var mediaType by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
var selectedServer by mutableStateOf<ServersAvailable?>(null)
|
var selectedServer by mutableStateOf<ServerOption?>(null)
|
||||||
var alt by mutableStateOf("")
|
var alt by mutableStateOf("")
|
||||||
var sensitiveContent by mutableStateOf(false)
|
var sensitiveContent by mutableStateOf(false)
|
||||||
|
|
||||||
@ -43,20 +48,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
this.account = account
|
this.account = account
|
||||||
this.galleryUri = uri
|
this.galleryUri = uri
|
||||||
this.mediaType = contentType
|
this.mediaType = contentType
|
||||||
this.selectedServer = defaultServer()
|
this.selectedServer = ServerOption(defaultServer(), false)
|
||||||
|
|
||||||
// if (selectedServer == ServersAvailable.IMGUR) {
|
|
||||||
// selectedServer = ServersAvailable.IMGUR_NIP_94
|
|
||||||
// } else
|
|
||||||
if (selectedServer == ServersAvailable.NOSTRIMG) {
|
|
||||||
selectedServer = ServersAvailable.NOSTRIMG_NIP_94
|
|
||||||
} else if (selectedServer == ServersAvailable.NOSTR_BUILD) {
|
|
||||||
selectedServer = ServersAvailable.NOSTR_BUILD_NIP_94
|
|
||||||
} else if (selectedServer == ServersAvailable.NOSTRFILES_DEV) {
|
|
||||||
selectedServer = ServersAvailable.NOSTRFILES_DEV_NIP_94
|
|
||||||
} else if (selectedServer == ServersAvailable.NOSTRCHECK_ME) {
|
|
||||||
selectedServer = ServersAvailable.NOSTRCHECK_ME_NIP_94
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upload(context: Context, relayList: List<Relay>? = null) {
|
fun upload(context: Context, relayList: List<Relay>? = null) {
|
||||||
@ -76,8 +68,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
contentType,
|
contentType,
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
onReady = { fileUri, contentType, size ->
|
onReady = { fileUri, contentType, size ->
|
||||||
|
if (serverToUse.isNip95) {
|
||||||
if (selectedServer == ServersAvailable.NIP95) {
|
|
||||||
uploadingPercentage.value = 0.2f
|
uploadingPercentage.value = 0.2f
|
||||||
uploadingDescription.value = "Loading"
|
uploadingDescription.value = "Loading"
|
||||||
contentResolver.openInputStream(fileUri)?.use {
|
contentResolver.openInputStream(fileUri)?.use {
|
||||||
@ -95,30 +86,35 @@ open class NewMediaModel : ViewModel() {
|
|||||||
uploadingPercentage.value = 0.2f
|
uploadingPercentage.value = 0.2f
|
||||||
uploadingDescription.value = "Uploading"
|
uploadingDescription.value = "Uploading"
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ImageUploader(account).uploadImage(
|
try {
|
||||||
uri = fileUri,
|
val result = Nip96Uploader(account).uploadImage(
|
||||||
contentType = contentType,
|
uri = fileUri,
|
||||||
size = size,
|
contentType = contentType,
|
||||||
server = serverToUse,
|
size = size,
|
||||||
contentResolver = contentResolver,
|
alt = alt,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
sensitiveContent = if (sensitiveContent) "" else null,
|
||||||
createNIP94Record(
|
server = serverToUse.server,
|
||||||
imageUrl,
|
contentResolver = contentResolver,
|
||||||
mimeType,
|
onProgress = { percent: Float ->
|
||||||
alt,
|
uploadingPercentage.value = 0.2f + (0.2f * percent)
|
||||||
sensitiveContent,
|
|
||||||
relayList = relayList
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
isUploadingImage = false
|
|
||||||
uploadingPercentage.value = 0.00f
|
|
||||||
uploadingDescription.value = null
|
|
||||||
viewModelScope.launch {
|
|
||||||
imageUploadingError.emit("Failed to upload the image / video")
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
createNIP94Record(
|
||||||
|
uploadingResult = result,
|
||||||
|
localContentType = contentType,
|
||||||
|
alt = alt,
|
||||||
|
sensitiveContent = sensitiveContent,
|
||||||
|
relayList = relayList
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
isUploadingImage = false
|
||||||
|
uploadingPercentage.value = 0.00f
|
||||||
|
uploadingDescription.value = null
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload: ${e.message}")
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -142,63 +138,83 @@ open class NewMediaModel : ViewModel() {
|
|||||||
uploadingPercentage.value = 0.0f
|
uploadingPercentage.value = 0.0f
|
||||||
|
|
||||||
alt = ""
|
alt = ""
|
||||||
selectedServer = account?.defaultFileServer
|
selectedServer = ServerOption(defaultServer(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canPost(): Boolean {
|
fun canPost(): Boolean {
|
||||||
return !isUploadingImage && galleryUri != null && selectedServer != null
|
return !isUploadingImage && galleryUri != null && selectedServer != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNIP94Record(imageUrl: String, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
suspend fun createNIP94Record(
|
||||||
|
uploadingResult: Nip96Uploader.PartialEvent,
|
||||||
|
localContentType: String?,
|
||||||
|
alt: String,
|
||||||
|
sensitiveContent: Boolean,
|
||||||
|
relayList: List<Relay>? = null
|
||||||
|
) {
|
||||||
uploadingPercentage.value = 0.40f
|
uploadingPercentage.value = 0.40f
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
uploadingDescription.value = "Server Processing"
|
||||||
uploadingDescription.value = "Server Processing"
|
// Images don't seem to be ready immediately after upload
|
||||||
// Images don't seem to be ready immediately after upload
|
|
||||||
|
|
||||||
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(imageUrl)
|
val imageUrl = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "url" }?.get(1)
|
||||||
|
val remoteMimeType = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null }
|
||||||
|
val originalHash = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "ox" }?.get(1)?.ifBlank { null }
|
||||||
|
val dim = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null }
|
||||||
|
val magnet = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "magnet" }?.get(1)?.ifBlank { null }
|
||||||
|
|
||||||
uploadingDescription.value = "Downloading"
|
if (imageUrl.isNullOrBlank()) {
|
||||||
uploadingPercentage.value = 0.60f
|
Log.e("ImageDownload", "Couldn't download image from server")
|
||||||
|
cancel()
|
||||||
|
uploadingPercentage.value = 0.00f
|
||||||
|
uploadingDescription.value = null
|
||||||
|
isUploadingImage = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (imageData != null) {
|
uploadingDescription.value = "Downloading"
|
||||||
uploadingPercentage.value = 0.80f
|
uploadingPercentage.value = 0.60f
|
||||||
uploadingDescription.value = "Hashing"
|
|
||||||
|
|
||||||
FileHeader.prepare(
|
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(imageUrl)
|
||||||
imageData,
|
|
||||||
imageUrl,
|
if (imageData != null) {
|
||||||
mimeType,
|
uploadingPercentage.value = 0.80f
|
||||||
alt,
|
uploadingDescription.value = "Hashing"
|
||||||
sensitiveContent,
|
|
||||||
onReady = {
|
FileHeader.prepare(
|
||||||
uploadingPercentage.value = 0.90f
|
data = imageData,
|
||||||
uploadingDescription.value = "Sending"
|
mimeType = remoteMimeType ?: localContentType,
|
||||||
account?.sendHeader(it, relayList) {
|
dimPrecomputed = dim,
|
||||||
uploadingPercentage.value = 1.00f
|
onReady = {
|
||||||
isUploadingImage = false
|
uploadingPercentage.value = 0.90f
|
||||||
onceUploaded()
|
uploadingDescription.value = "Sending"
|
||||||
cancel()
|
account?.sendHeader(imageUrl, magnet, it, alt, sensitiveContent, originalHash, relayList) {
|
||||||
}
|
uploadingPercentage.value = 1.00f
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
cancel()
|
|
||||||
uploadingPercentage.value = 0.00f
|
|
||||||
uploadingDescription.value = null
|
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
viewModelScope.launch {
|
onceUploaded()
|
||||||
imageUploadingError.emit("Failed to upload the image / video")
|
cancel()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
cancel()
|
||||||
|
uploadingPercentage.value = 0.00f
|
||||||
|
uploadingDescription.value = null
|
||||||
|
isUploadingImage = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
}
|
}
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Log.e("ImageDownload", "Couldn't download image from server")
|
|
||||||
cancel()
|
|
||||||
uploadingPercentage.value = 0.00f
|
|
||||||
uploadingDescription.value = null
|
|
||||||
isUploadingImage = false
|
|
||||||
viewModelScope.launch {
|
|
||||||
imageUploadingError.emit("Failed to upload the image / video")
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Log.e("ImageDownload", "Couldn't download image from server")
|
||||||
|
cancel()
|
||||||
|
uploadingPercentage.value = 0.00f
|
||||||
|
uploadingDescription.value = null
|
||||||
|
isUploadingImage = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,17 +226,15 @@ open class NewMediaModel : ViewModel() {
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
FileHeader.prepare(
|
FileHeader.prepare(
|
||||||
bytes,
|
bytes,
|
||||||
"",
|
|
||||||
mimeType,
|
mimeType,
|
||||||
alt,
|
null,
|
||||||
sensitiveContent,
|
|
||||||
onReady = {
|
onReady = {
|
||||||
uploadingDescription.value = "Signing"
|
uploadingDescription.value = "Signing"
|
||||||
uploadingPercentage.value = 0.40f
|
uploadingPercentage.value = 0.40f
|
||||||
account?.createNip95(bytes, headerInfo = it) { nip95 ->
|
account?.createNip95(bytes, headerInfo = it, alt, sensitiveContent) { nip95 ->
|
||||||
uploadingDescription.value = "Sending"
|
uploadingDescription.value = "Sending"
|
||||||
uploadingPercentage.value = 0.60f
|
uploadingPercentage.value = 0.60f
|
||||||
account?.sendNip95(nip95.first, nip95.second, relayList)
|
account?.consumeAndSendNip95(nip95.first, nip95.second, relayList)
|
||||||
|
|
||||||
uploadingPercentage.value = 1.00f
|
uploadingPercentage.value = 1.00f
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
@ -243,7 +257,7 @@ open class NewMediaModel : ViewModel() {
|
|||||||
|
|
||||||
fun isImage() = mediaType?.startsWith("image")
|
fun isImage() = mediaType?.startsWith("image")
|
||||||
fun isVideo() = mediaType?.startsWith("video")
|
fun isVideo() = mediaType?.startsWith("video")
|
||||||
fun defaultServer() = account?.defaultFileServer
|
fun defaultServer() = account?.defaultFileServer ?: Nip96MediaServers.DEFAULT[0]
|
||||||
fun onceUploaded(onceUploaded: () -> Unit) {
|
fun onceUploaded(onceUploaded: () -> Unit) {
|
||||||
this.onceUploaded = onceUploaded
|
this.onceUploaded = onceUploaded
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||||
@ -146,7 +146,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
|||||||
onPost = {
|
onPost = {
|
||||||
onClose()
|
onClose()
|
||||||
postViewModel.upload(context, relayList)
|
postViewModel.upload(context, relayList)
|
||||||
postViewModel.selectedServer?.let { account.changeDefaultFileServer(it) }
|
postViewModel.selectedServer?.let { account.changeDefaultFileServer(it.server) }
|
||||||
},
|
},
|
||||||
isActive = postViewModel.canPost()
|
isActive = postViewModel.canPost()
|
||||||
)
|
)
|
||||||
@ -170,26 +170,19 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNIP94Server(selectedServer: ServersAvailable?): Boolean {
|
|
||||||
return selectedServer == ServersAvailable.NOSTRIMG_NIP_94 ||
|
|
||||||
// selectedServer == ServersAvailable.IMGUR_NIP_94 ||
|
|
||||||
selectedServer == ServersAvailable.NOSTR_BUILD_NIP_94 ||
|
|
||||||
selectedServer == ServersAvailable.NOSTRFILES_DEV_NIP_94 ||
|
|
||||||
selectedServer == ServersAvailable.NOSTRCHECK_ME_NIP_94
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewModel) {
|
fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewModel) {
|
||||||
val fileServers = listOf(
|
val fileServers = Nip96MediaServers.DEFAULT.map { ServerOption(it, false) } + listOf(
|
||||||
// Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)),
|
ServerOption(
|
||||||
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
|
Nip96MediaServers.ServerName(
|
||||||
Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)),
|
"NIP95",
|
||||||
Triple(ServersAvailable.NOSTRFILES_DEV_NIP_94, stringResource(id = R.string.upload_server_nostrfilesdev_nip94), stringResource(id = R.string.upload_server_nostrfilesdev_nip94_explainer)),
|
stringResource(id = R.string.upload_server_relays_nip95)
|
||||||
Triple(ServersAvailable.NOSTRCHECK_ME_NIP_94, stringResource(id = R.string.upload_server_nostrcheckme_nip94), stringResource(id = R.string.upload_server_nostrcheckme_nip94_explainer)),
|
),
|
||||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.server.name, it.server.baseUrl) }.toImmutableList() }
|
||||||
val resolver = LocalContext.current.contentResolver
|
val resolver = LocalContext.current.contentResolver
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@ -252,10 +245,12 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
|||||||
) {
|
) {
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
label = stringResource(id = R.string.file_server),
|
label = stringResource(id = R.string.file_server),
|
||||||
placeholder = fileServers.firstOrNull { it.first == accountViewModel.account.defaultFileServer }?.second ?: fileServers[0].second,
|
placeholder = fileServers.firstOrNull {
|
||||||
|
it.server == accountViewModel.account.defaultFileServer
|
||||||
|
}?.server?.name ?: fileServers[0].server.name,
|
||||||
options = fileServerOptions,
|
options = fileServerOptions,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
postViewModel.selectedServer = fileServers[it].first
|
postViewModel.selectedServer = fileServers[it]
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||||
@ -263,44 +258,40 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNIP94Server(postViewModel.selectedServer) ||
|
Row(
|
||||||
postViewModel.selectedServer == ServersAvailable.NIP95
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Row(
|
SettingSwitchItem(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
checked = postViewModel.sensitiveContent,
|
||||||
modifier = Modifier.fillMaxWidth()
|
onCheckedChange = { postViewModel.sensitiveContent = it },
|
||||||
) {
|
title = R.string.add_sensitive_content_label,
|
||||||
SettingSwitchItem(
|
description = R.string.add_sensitive_content_description
|
||||||
checked = postViewModel.sensitiveContent,
|
)
|
||||||
onCheckedChange = { postViewModel.sensitiveContent = it },
|
}
|
||||||
title = R.string.add_sensitive_content_label,
|
|
||||||
description = R.string.add_sensitive_content_description
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringResource(R.string.content_description)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
||||||
) {
|
value = postViewModel.alt,
|
||||||
OutlinedTextField(
|
onValueChange = { postViewModel.alt = it },
|
||||||
label = { Text(text = stringResource(R.string.content_description)) },
|
placeholder = {
|
||||||
modifier = Modifier
|
Text(
|
||||||
.fillMaxWidth()
|
text = stringResource(R.string.content_description_example),
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
color = MaterialTheme.colorScheme.placeholderText
|
||||||
value = postViewModel.alt,
|
|
||||||
onValueChange = { postViewModel.alt = it },
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.content_description_example),
|
|
||||||
color = MaterialTheme.colorScheme.placeholderText
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(
|
|
||||||
capitalization = KeyboardCapitalization.Sentences
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,8 +112,8 @@ import com.google.accompanist.permissions.isGranted
|
|||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
||||||
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
|
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
|
||||||
@ -404,8 +404,8 @@ fun NewPostView(
|
|||||||
url,
|
url,
|
||||||
accountViewModel.account.defaultFileServer,
|
accountViewModel.account.defaultFileServer,
|
||||||
onAdd = { alt, server, sensitiveContent ->
|
onAdd = { alt, server, sensitiveContent ->
|
||||||
postViewModel.upload(url, alt, sensitiveContent, server, context, relayList)
|
postViewModel.upload(url, alt, sensitiveContent, false, server, context, relayList)
|
||||||
accountViewModel.account.changeDefaultFileServer(server)
|
accountViewModel.account.changeDefaultFileServer(server.server)
|
||||||
},
|
},
|
||||||
onCancel = {
|
onCancel = {
|
||||||
postViewModel.contentToAddUrl = null
|
postViewModel.contentToAddUrl = null
|
||||||
@ -1597,8 +1597,8 @@ fun CreateButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
|||||||
@Composable
|
@Composable
|
||||||
fun ImageVideoDescription(
|
fun ImageVideoDescription(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
defaultServer: ServersAvailable,
|
defaultServer: Nip96MediaServers.ServerName,
|
||||||
onAdd: (String, ServersAvailable, Boolean) -> Unit,
|
onAdd: (String, ServerOption, Boolean) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onError: (String) -> Unit,
|
onError: (String) -> Unit,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
@ -1609,23 +1609,19 @@ fun ImageVideoDescription(
|
|||||||
val isImage = mediaType.startsWith("image")
|
val isImage = mediaType.startsWith("image")
|
||||||
val isVideo = mediaType.startsWith("video")
|
val isVideo = mediaType.startsWith("video")
|
||||||
|
|
||||||
val fileServers = listOf(
|
val fileServers = Nip96MediaServers.DEFAULT.map { ServerOption(it, false) } + listOf(
|
||||||
// Triple(ServersAvailable.IMGUR, stringResource(id = R.string.upload_server_imgur), stringResource(id = R.string.upload_server_imgur_explainer)),
|
ServerOption(
|
||||||
Triple(ServersAvailable.NOSTRIMG, stringResource(id = R.string.upload_server_nostrimg), stringResource(id = R.string.upload_server_nostrimg_explainer)),
|
Nip96MediaServers.ServerName(
|
||||||
Triple(ServersAvailable.NOSTR_BUILD, stringResource(id = R.string.upload_server_nostrbuild), stringResource(id = R.string.upload_server_nostrbuild_explainer)),
|
"NIP95",
|
||||||
Triple(ServersAvailable.NOSTRFILES_DEV, stringResource(id = R.string.upload_server_nostrfilesdev), stringResource(id = R.string.upload_server_nostrfilesdev_explainer)),
|
stringResource(id = R.string.upload_server_relays_nip95)
|
||||||
Triple(ServersAvailable.NOSTRCHECK_ME, stringResource(id = R.string.upload_server_nostrcheckme), stringResource(id = R.string.upload_server_nostrcheckme_explainer)),
|
),
|
||||||
// Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)),
|
true
|
||||||
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
|
)
|
||||||
Triple(ServersAvailable.NOSTR_BUILD_NIP_94, stringResource(id = R.string.upload_server_nostrbuild_nip94), stringResource(id = R.string.upload_server_nostrbuild_nip94_explainer)),
|
|
||||||
Triple(ServersAvailable.NOSTRFILES_DEV_NIP_94, stringResource(id = R.string.upload_server_nostrfilesdev_nip94), stringResource(id = R.string.upload_server_nostrfilesdev_nip94_explainer)),
|
|
||||||
Triple(ServersAvailable.NOSTRCHECK_ME_NIP_94, stringResource(id = R.string.upload_server_nostrcheckme_nip94), stringResource(id = R.string.upload_server_nostrcheckme_nip94_explainer)),
|
|
||||||
Triple(ServersAvailable.NIP95, stringResource(id = R.string.upload_server_relays_nip95), stringResource(id = R.string.upload_server_relays_nip95_explainer))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.second, it.third) }.toImmutableList() }
|
val fileServerOptions = remember { fileServers.map { TitleExplainer(it.server.name, it.server.baseUrl) }.toImmutableList() }
|
||||||
|
|
||||||
var selectedServer by remember { mutableStateOf(defaultServer) }
|
var selectedServer by remember { mutableStateOf(ServerOption(defaultServer, false)) }
|
||||||
var message by remember { mutableStateOf("") }
|
var message by remember { mutableStateOf("") }
|
||||||
var sensitiveContent by remember { mutableStateOf(false) }
|
var sensitiveContent by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@ -1735,10 +1731,12 @@ fun ImageVideoDescription(
|
|||||||
) {
|
) {
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
label = stringResource(id = R.string.file_server),
|
label = stringResource(id = R.string.file_server),
|
||||||
placeholder = fileServers.filter { it.first == defaultServer }.firstOrNull()?.second ?: fileServers[0].second,
|
placeholder = fileServers.firstOrNull {
|
||||||
|
it.server == accountViewModel.account.defaultFileServer
|
||||||
|
}?.server?.name ?: fileServers[0].server.name,
|
||||||
options = fileServerOptions,
|
options = fileServerOptions,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
selectedServer = fileServers[it].first
|
selectedServer = fileServers[it]
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||||
@ -1746,45 +1744,41 @@ fun ImageVideoDescription(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNIP94Server(selectedServer) ||
|
Row(
|
||||||
selectedServer == ServersAvailable.NIP95
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Row(
|
SettingSwitchItem(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
checked = sensitiveContent,
|
||||||
modifier = Modifier.fillMaxWidth()
|
onCheckedChange = { sensitiveContent = it },
|
||||||
) {
|
title = R.string.add_sensitive_content_label,
|
||||||
SettingSwitchItem(
|
description = R.string.add_sensitive_content_description
|
||||||
checked = sensitiveContent,
|
)
|
||||||
onCheckedChange = { sensitiveContent = it },
|
}
|
||||||
title = R.string.add_sensitive_content_label,
|
|
||||||
description = R.string.add_sensitive_content_description
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringResource(R.string.content_description)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
||||||
) {
|
value = message,
|
||||||
OutlinedTextField(
|
onValueChange = { message = it },
|
||||||
label = { Text(text = stringResource(R.string.content_description)) },
|
placeholder = {
|
||||||
modifier = Modifier
|
Text(
|
||||||
.fillMaxWidth()
|
text = stringResource(R.string.content_description_example),
|
||||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
color = MaterialTheme.colorScheme.placeholderText
|
||||||
value = message,
|
|
||||||
onValueChange = { message = it },
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.content_description_example),
|
|
||||||
color = MaterialTheme.colorScheme.placeholderText
|
|
||||||
)
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(
|
|
||||||
capitalization = KeyboardCapitalization.Sentences
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
capitalization = KeyboardCapitalization.Sentences
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
|
@ -18,7 +18,6 @@ import com.fonfon.kgeohash.toGeoHash
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import com.vitorpamplona.amethyst.service.LocationUtil
|
import com.vitorpamplona.amethyst.service.LocationUtil
|
||||||
@ -35,10 +34,14 @@ import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
|||||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.FileHeaderEvent
|
||||||
|
import com.vitorpamplona.quartz.events.FileStorageEvent
|
||||||
|
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.Price
|
import com.vitorpamplona.quartz.events.Price
|
||||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||||
|
import com.vitorpamplona.quartz.events.findURLs
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -63,6 +66,9 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
var mentions by mutableStateOf<List<User>?>(null)
|
var mentions by mutableStateOf<List<User>?>(null)
|
||||||
var replyTos by mutableStateOf<List<Note>?>(null)
|
var replyTos by mutableStateOf<List<Note>?>(null)
|
||||||
|
|
||||||
|
var nip94attachments by mutableStateOf<List<FileHeaderEvent>>(emptyList())
|
||||||
|
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||||
|
|
||||||
var message by mutableStateOf(TextFieldValue(""))
|
var message by mutableStateOf(TextFieldValue(""))
|
||||||
var urlPreview by mutableStateOf<String?>(null)
|
var urlPreview by mutableStateOf<String?>(null)
|
||||||
var isUploadingImage by mutableStateOf(false)
|
var isUploadingImage by mutableStateOf(false)
|
||||||
@ -219,11 +225,25 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
|
|
||||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
|
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount else null
|
||||||
|
|
||||||
|
nip95attachments.forEach {
|
||||||
|
if (replyTos?.contains(LocalCache.getNoteIfExists(it.second.id)) == true) {
|
||||||
|
account?.sendNip95(it.first, it.second, relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val urls = findURLs(tagger.message)
|
||||||
|
val usedAttachments = nip94attachments.filter {
|
||||||
|
it.urls().intersect(urls).isNotEmpty()
|
||||||
|
}
|
||||||
|
usedAttachments.forEach {
|
||||||
|
account?.sendHeader(it, relayList, { })
|
||||||
|
}
|
||||||
|
|
||||||
if (originalNote?.channelHex() != null) {
|
if (originalNote?.channelHex() != null) {
|
||||||
if (originalNote is AddressableEvent && originalNote?.address() != null) {
|
if (originalNote is AddressableEvent && originalNote?.address() != null) {
|
||||||
account?.sendLiveMessage(tagger.message, originalNote?.address()!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
|
account?.sendLiveMessage(tagger.message, originalNote?.address()!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash, nip94attachments = usedAttachments)
|
||||||
} else {
|
} else {
|
||||||
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
|
account?.sendChannelMessage(tagger.message, tagger.channelHex!!, tagger.replyTos, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash, nip94attachments = usedAttachments)
|
||||||
}
|
}
|
||||||
} else if (originalNote?.event is PrivateDmEvent) {
|
} else if (originalNote?.event is PrivateDmEvent) {
|
||||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
|
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
|
||||||
@ -281,7 +301,8 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
wantsToMarkAsSensitive,
|
wantsToMarkAsSensitive,
|
||||||
localZapRaiserAmount,
|
localZapRaiserAmount,
|
||||||
relayList,
|
relayList,
|
||||||
geoHash
|
geoHash,
|
||||||
|
nip94attachments = usedAttachments
|
||||||
)
|
)
|
||||||
} else if (wantsProduct) {
|
} else if (wantsProduct) {
|
||||||
account?.sendClassifieds(
|
account?.sendClassifieds(
|
||||||
@ -298,7 +319,8 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||||
zapRaiserAmount = localZapRaiserAmount,
|
zapRaiserAmount = localZapRaiserAmount,
|
||||||
relayList = relayList,
|
relayList = relayList,
|
||||||
geohash = geoHash
|
geohash = geoHash,
|
||||||
|
nip94attachments = usedAttachments
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// adds markers
|
// adds markers
|
||||||
@ -322,7 +344,8 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
root = rootId,
|
root = rootId,
|
||||||
directMentions = tagger.directMentions,
|
directMentions = tagger.directMentions,
|
||||||
relayList = relayList,
|
relayList = relayList,
|
||||||
geohash = geoHash
|
geohash = geoHash,
|
||||||
|
nip94attachments = usedAttachments
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -330,7 +353,15 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upload(galleryUri: Uri, alt: String, sensitiveContent: Boolean, server: ServersAvailable, context: Context, relayList: List<Relay>? = null) {
|
fun upload(
|
||||||
|
galleryUri: Uri,
|
||||||
|
alt: String?,
|
||||||
|
sensitiveContent: Boolean,
|
||||||
|
isPrivate: Boolean = false,
|
||||||
|
server: ServerOption,
|
||||||
|
context: Context,
|
||||||
|
relayList: List<Relay>? = null
|
||||||
|
) {
|
||||||
isUploadingImage = true
|
isUploadingImage = true
|
||||||
contentToAddUrl = null
|
contentToAddUrl = null
|
||||||
|
|
||||||
@ -343,35 +374,49 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
contentType,
|
contentType,
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
onReady = { fileUri, contentType, size ->
|
onReady = { fileUri, contentType, size ->
|
||||||
if (server == ServersAvailable.NIP95) {
|
if (server.isNip95) {
|
||||||
contentResolver.openInputStream(fileUri)?.use {
|
contentResolver.openInputStream(fileUri)?.use {
|
||||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ImageUploader(account).uploadImage(
|
try {
|
||||||
uri = fileUri,
|
val result = Nip96Uploader(account).uploadImage(
|
||||||
contentType = contentType,
|
uri = fileUri,
|
||||||
size = size,
|
contentType = contentType,
|
||||||
server = server,
|
size = size,
|
||||||
contentResolver = contentResolver,
|
alt = alt,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
sensitiveContent = if (sensitiveContent) "" else null,
|
||||||
if (isNIP94Server(server)) {
|
server = server.server,
|
||||||
createNIP94Record(imageUrl, mimeType, alt, sensitiveContent)
|
contentResolver = contentResolver,
|
||||||
} else {
|
onProgress = { }
|
||||||
isUploadingImage = false
|
)
|
||||||
message = TextFieldValue(message.text + "\n" + imageUrl)
|
|
||||||
urlPreview = findUrlInMessage()
|
if (!isPrivate) {
|
||||||
}
|
createNIP94Record(
|
||||||
},
|
uploadingResult = result,
|
||||||
onError = {
|
localContentType = contentType,
|
||||||
Log.e("ImageUploader", "Failed to upload the image / video", it)
|
alt = alt,
|
||||||
|
sensitiveContent = sensitiveContent
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val url = result.tags?.firstOrNull() { it.size > 1 && it[0] == "url" }?.get(1)
|
||||||
|
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
viewModelScope.launch {
|
message = TextFieldValue(message.text + "\n" + url)
|
||||||
imageUploadingError.emit("Failed to upload the image / video")
|
urlPreview = findUrlInMessage()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
} catch (e: Exception) {
|
||||||
|
Log.e(
|
||||||
|
"ImageUploader",
|
||||||
|
"Failed to upload ${e.message}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
isUploadingImage = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -576,44 +621,66 @@ open class NewPostViewModel() : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNIP94Record(imageUrl: String, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
suspend fun createNIP94Record(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
uploadingResult: Nip96Uploader.PartialEvent,
|
||||||
// Images don't seem to be ready immediately after upload
|
localContentType: String?,
|
||||||
FileHeader.prepare(
|
alt: String?,
|
||||||
imageUrl,
|
sensitiveContent: Boolean
|
||||||
mimeType,
|
) {
|
||||||
alt,
|
// Images don't seem to be ready immediately after upload
|
||||||
sensitiveContent,
|
val imageUrl = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "url" }?.get(1)
|
||||||
onReady = {
|
val remoteMimeType = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "m" }?.get(1)?.ifBlank { null }
|
||||||
account?.sendHeader(it, relayList = relayList) { note ->
|
val originalHash = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "ox" }?.get(1)?.ifBlank { null }
|
||||||
isUploadingImage = false
|
val dim = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "dim" }?.get(1)?.ifBlank { null }
|
||||||
|
val magnet = uploadingResult.tags?.firstOrNull() { it.size > 1 && it[0] == "magnet" }?.get(1)?.ifBlank { null }
|
||||||
|
|
||||||
message = TextFieldValue(message.text + "\nnostr:" + note.toNEvent())
|
if (imageUrl.isNullOrBlank()) {
|
||||||
|
Log.e("ImageDownload", "Couldn't download image from server")
|
||||||
urlPreview = findUrlInMessage()
|
cancel()
|
||||||
}
|
isUploadingImage = false
|
||||||
},
|
viewModelScope.launch {
|
||||||
onError = {
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
isUploadingImage = false
|
}
|
||||||
viewModelScope.launch {
|
return
|
||||||
imageUploadingError.emit("Failed to upload the image / video")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileHeader.prepare(
|
||||||
|
fileUrl = imageUrl,
|
||||||
|
mimeType = remoteMimeType ?: localContentType,
|
||||||
|
dimPrecomputed = dim,
|
||||||
|
onReady = { header: FileHeader ->
|
||||||
|
account?.createHeader(imageUrl, magnet, header, alt, sensitiveContent, originalHash) { event ->
|
||||||
|
isUploadingImage = false
|
||||||
|
nip94attachments = nip94attachments + event
|
||||||
|
message = TextFieldValue(message.text + "\n" + imageUrl)
|
||||||
|
urlPreview = findUrlInMessage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
isUploadingImage = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNIP95Record(bytes: ByteArray, mimeType: String?, alt: String, sensitiveContent: Boolean, relayList: List<Relay>? = null) {
|
fun createNIP95Record(
|
||||||
|
bytes: ByteArray,
|
||||||
|
mimeType: String?,
|
||||||
|
alt: String?,
|
||||||
|
sensitiveContent: Boolean,
|
||||||
|
relayList: List<Relay>? = null
|
||||||
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
FileHeader.prepare(
|
FileHeader.prepare(
|
||||||
bytes,
|
bytes,
|
||||||
"",
|
|
||||||
mimeType,
|
mimeType,
|
||||||
alt,
|
null,
|
||||||
sensitiveContent,
|
|
||||||
onReady = {
|
onReady = {
|
||||||
account?.createNip95(bytes, headerInfo = it) { nip95 ->
|
account?.createNip95(bytes, headerInfo = it, alt, sensitiveContent) { nip95 ->
|
||||||
val note = nip95.let { it1 -> account?.sendNip95(it1.first, it1.second, relayList = relayList) }
|
nip95attachments = nip95attachments + nip95
|
||||||
|
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
|
||||||
|
|
||||||
isUploadingImage = false
|
isUploadingImage = false
|
||||||
|
|
||||||
|
@ -184,23 +184,35 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
onReady = { fileUri, contentType, size ->
|
onReady = { fileUri, contentType, size ->
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
ImageUploader(account).uploadImage(
|
try {
|
||||||
uri = fileUri,
|
val result = Nip96Uploader(account).uploadImage(
|
||||||
contentType = contentType,
|
uri = fileUri,
|
||||||
size = size,
|
contentType = contentType,
|
||||||
server = account.defaultFileServer,
|
size = size,
|
||||||
contentResolver = contentResolver,
|
alt = null,
|
||||||
onSuccess = { imageUrl, mimeType ->
|
sensitiveContent = null,
|
||||||
|
server = account.defaultFileServer,
|
||||||
|
contentResolver = contentResolver,
|
||||||
|
onProgress = { }
|
||||||
|
)
|
||||||
|
|
||||||
|
val url = result.tags?.firstOrNull() { it.size > 1 && it[0] == "url" }?.get(1)
|
||||||
|
|
||||||
|
if (url != null) {
|
||||||
onUploading(false)
|
onUploading(false)
|
||||||
onUploaded(imageUrl)
|
onUploaded(url)
|
||||||
},
|
} else {
|
||||||
onError = {
|
|
||||||
onUploading(false)
|
onUploading(false)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageUploadingError.emit("Failed to upload the image / video")
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
} catch (e: Exception) {
|
||||||
|
onUploading(false)
|
||||||
|
viewModelScope.launch {
|
||||||
|
imageUploadingError.emit("Failed to upload the image / video")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
|
@ -0,0 +1,237 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.service.HttpClient
|
||||||
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
|
import com.vitorpamplona.amethyst.service.Nip96Retriever
|
||||||
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okio.BufferedSink
|
||||||
|
import okio.source
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.Base64
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
|
||||||
|
|
||||||
|
fun randomChars() = List(16) { charPool.random() }.joinToString("")
|
||||||
|
|
||||||
|
class Nip96Uploader(val account: Account?) {
|
||||||
|
|
||||||
|
suspend fun uploadImage(
|
||||||
|
uri: Uri,
|
||||||
|
contentType: String?,
|
||||||
|
size: Long?,
|
||||||
|
alt: String?,
|
||||||
|
sensitiveContent: String?,
|
||||||
|
server: Nip96MediaServers.ServerName,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
onProgress: (percentage: Float) -> Unit
|
||||||
|
): PartialEvent {
|
||||||
|
val serverInfo = Nip96Retriever().loadInfo(
|
||||||
|
server.baseUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
return uploadImage(uri, contentType, size, alt, sensitiveContent, serverInfo, contentResolver, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadImage(
|
||||||
|
uri: Uri,
|
||||||
|
contentType: String?,
|
||||||
|
size: Long?,
|
||||||
|
alt: String?,
|
||||||
|
sensitiveContent: String?,
|
||||||
|
server: Nip96Retriever.ServerInfo,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
onProgress: (percentage: Float) -> Unit
|
||||||
|
): PartialEvent {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
val myContentType = contentType ?: contentResolver.getType(uri)
|
||||||
|
val imageInputStream = contentResolver.openInputStream(uri)
|
||||||
|
|
||||||
|
val length = size ?: contentResolver.query(uri, null, null, null, null)?.use {
|
||||||
|
it.moveToFirst()
|
||||||
|
val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
|
||||||
|
it.getLong(sizeIndex)
|
||||||
|
} ?: kotlin.runCatching {
|
||||||
|
uri.toFile().length()
|
||||||
|
}.getOrNull() ?: 0
|
||||||
|
|
||||||
|
checkNotNull(imageInputStream) {
|
||||||
|
"Can't open the image input stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadImage(imageInputStream, length, myContentType, alt, sensitiveContent, server, onProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadImage(
|
||||||
|
inputStream: InputStream,
|
||||||
|
length: Long,
|
||||||
|
contentType: String?,
|
||||||
|
alt: String?,
|
||||||
|
sensitiveContent: String?,
|
||||||
|
server: Nip96Retriever.ServerInfo,
|
||||||
|
onProgress: (percentage: Float) -> Unit
|
||||||
|
): PartialEvent {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
val fileName = randomChars()
|
||||||
|
val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: ""
|
||||||
|
|
||||||
|
val client = HttpClient.getHttpClient()
|
||||||
|
val requestBody: RequestBody
|
||||||
|
val requestBuilder = Request.Builder()
|
||||||
|
|
||||||
|
requestBody = MultipartBody.Builder()
|
||||||
|
.setType(MultipartBody.FORM)
|
||||||
|
.addFormDataPart("expiration", "")
|
||||||
|
.addFormDataPart("size", length.toString())
|
||||||
|
.also { body ->
|
||||||
|
alt?.let { body.addFormDataPart("alt", it) }
|
||||||
|
sensitiveContent?.let { body.addFormDataPart("content-warning", it) }
|
||||||
|
contentType?.let { body.addFormDataPart("content_type", it) }
|
||||||
|
}
|
||||||
|
.addFormDataPart(
|
||||||
|
"file",
|
||||||
|
"$fileName.$extension",
|
||||||
|
|
||||||
|
object : RequestBody() {
|
||||||
|
override fun contentType() = contentType?.toMediaType()
|
||||||
|
|
||||||
|
override fun contentLength() = length
|
||||||
|
|
||||||
|
override fun writeTo(sink: BufferedSink) {
|
||||||
|
inputStream.source().use(sink::writeAll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
nip98Header(server.apiUrl)?.let {
|
||||||
|
requestBuilder.addHeader("Authorization", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBuilder
|
||||||
|
.addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.url(server.apiUrl)
|
||||||
|
.post(requestBody)
|
||||||
|
|
||||||
|
val request = requestBuilder.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use { response ->
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
response.body.use { body ->
|
||||||
|
val str = body.string()
|
||||||
|
val result = parseResults(str)
|
||||||
|
|
||||||
|
if (!result.processingUrl.isNullOrBlank()) {
|
||||||
|
return waitProcessing(result, server, onProgress)
|
||||||
|
} else if (result.status == "success" && result.nip94Event != null) {
|
||||||
|
return result.nip94Event
|
||||||
|
} else {
|
||||||
|
throw RuntimeException("Failed to upload with message: ${result.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw RuntimeException("Error Uploading image: ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun waitProcessing(
|
||||||
|
result: Nip96Result,
|
||||||
|
server: Nip96Retriever.ServerInfo,
|
||||||
|
onProgress: (percentage: Float) -> Unit
|
||||||
|
): PartialEvent {
|
||||||
|
val client = HttpClient.getHttpClient()
|
||||||
|
var currentResult = result
|
||||||
|
|
||||||
|
while (!result.processingUrl.isNullOrBlank() && (currentResult.percentage ?: 100) < 100) {
|
||||||
|
onProgress((currentResult.percentage ?: 100) / 100f)
|
||||||
|
|
||||||
|
val request: Request = Request.Builder()
|
||||||
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.url(result.processingUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
it.body.use {
|
||||||
|
currentResult = parseResults(it.string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
onProgress((currentResult.percentage ?: 100) / 100f)
|
||||||
|
|
||||||
|
val nip94 = currentResult.nip94Event
|
||||||
|
|
||||||
|
if (nip94 != null) {
|
||||||
|
return nip94
|
||||||
|
} else {
|
||||||
|
throw RuntimeException("Error waiting for processing. Final result is unavailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun nip98Header(url: String): String? {
|
||||||
|
return withTimeoutOrNull(5000) {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
nip98Header(url, "POST") { authorizationToken ->
|
||||||
|
continuation.resume(authorizationToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nip98Header(url: String, method: String, file: ByteArray? = null, onReady: (String?) -> Unit) {
|
||||||
|
val myAccount = account
|
||||||
|
|
||||||
|
if (myAccount == null) {
|
||||||
|
onReady(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myAccount.createHTTPAuthorization(url, method, file) {
|
||||||
|
val encodedNIP98Event = Base64.getEncoder().encodeToString(it.toJson().toByteArray())
|
||||||
|
onReady("Nostr $encodedNIP98Event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Nip96Result(
|
||||||
|
val status: String,
|
||||||
|
val message: String? = null,
|
||||||
|
@JsonProperty("processing_url")
|
||||||
|
val processingUrl: String? = null,
|
||||||
|
val percentage: Int? = null,
|
||||||
|
@JsonProperty("nip94_event")
|
||||||
|
val nip94Event: PartialEvent? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
class PartialEvent(
|
||||||
|
val tags: Array<Array<String>>? = null,
|
||||||
|
val content: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseResults(body: String): Nip96Result {
|
||||||
|
val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
return mapper.readValue(body, Nip96Result::class.java)
|
||||||
|
}
|
||||||
|
}
|
@ -85,12 +85,12 @@ import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.ServerOption
|
||||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
import com.vitorpamplona.amethyst.ui.components.LoadNote
|
||||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||||
@ -401,24 +401,13 @@ fun EditFieldRow(
|
|||||||
tint = MaterialTheme.colorScheme.placeholderText,
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
modifier = EditFieldLeadingIconModifier
|
modifier = EditFieldLeadingIconModifier
|
||||||
) {
|
) {
|
||||||
val fileServer = if (isPrivate) {
|
channelScreenModel.upload(
|
||||||
// TODO: Make private servers
|
galleryUri = it,
|
||||||
when (accountViewModel.account.defaultFileServer) {
|
alt = null,
|
||||||
ServersAvailable.NOSTR_BUILD -> ServersAvailable.NOSTR_BUILD
|
sensitiveContent = false,
|
||||||
ServersAvailable.NOSTRIMG -> ServersAvailable.NOSTRIMG
|
server = ServerOption(accountViewModel.account.defaultFileServer, false),
|
||||||
ServersAvailable.NOSTRFILES_DEV -> ServersAvailable.NOSTRFILES_DEV
|
context = context
|
||||||
ServersAvailable.NOSTRCHECK_ME -> ServersAvailable.NOSTRCHECK_ME
|
)
|
||||||
ServersAvailable.NOSTR_BUILD_NIP_94 -> ServersAvailable.NOSTR_BUILD
|
|
||||||
ServersAvailable.NOSTRIMG_NIP_94 -> ServersAvailable.NOSTRIMG
|
|
||||||
ServersAvailable.NOSTRFILES_DEV_NIP_94 -> ServersAvailable.NOSTRFILES_DEV
|
|
||||||
ServersAvailable.NOSTRCHECK_ME_NIP_94 -> ServersAvailable.NOSTRCHECK_ME
|
|
||||||
ServersAvailable.NIP95 -> ServersAvailable.NOSTR_BUILD
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
accountViewModel.account.defaultFileServer
|
|
||||||
}
|
|
||||||
|
|
||||||
channelScreenModel.upload(it, "", false, fileServer, context)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
|
@ -65,12 +65,12 @@ import androidx.lifecycle.map
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.actions.ServerOption
|
||||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
@ -379,26 +379,14 @@ fun PrivateMessageEditFieldRow(
|
|||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
.padding(start = 2.dp)
|
.padding(start = 2.dp)
|
||||||
) {
|
) {
|
||||||
val fileServer = if (isPrivate) {
|
channelScreenModel.upload(
|
||||||
// TODO: Make private servers
|
galleryUri = it,
|
||||||
when (accountViewModel.account.defaultFileServer) {
|
alt = null,
|
||||||
ServersAvailable.NOSTR_BUILD -> ServersAvailable.NOSTR_BUILD
|
sensitiveContent = false,
|
||||||
ServersAvailable.NOSTRIMG -> ServersAvailable.NOSTRIMG
|
isPrivate = isPrivate,
|
||||||
ServersAvailable.NOSTRFILES_DEV -> ServersAvailable.NOSTRFILES_DEV
|
server = ServerOption(accountViewModel.account.defaultFileServer, false),
|
||||||
ServersAvailable.NOSTRCHECK_ME -> ServersAvailable.NOSTRCHECK_ME
|
context = context
|
||||||
|
)
|
||||||
ServersAvailable.NOSTR_BUILD_NIP_94 -> ServersAvailable.NOSTR_BUILD
|
|
||||||
ServersAvailable.NOSTRIMG_NIP_94 -> ServersAvailable.NOSTRIMG
|
|
||||||
ServersAvailable.NOSTRFILES_DEV_NIP_94 -> ServersAvailable.NOSTRFILES_DEV
|
|
||||||
ServersAvailable.NOSTRCHECK_ME_NIP_94 -> ServersAvailable.NOSTRCHECK_ME
|
|
||||||
|
|
||||||
ServersAvailable.NIP95 -> ServersAvailable.NOSTR_BUILD
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
accountViewModel.account.defaultFileServer
|
|
||||||
}
|
|
||||||
|
|
||||||
channelScreenModel.upload(it, "", false, fileServer, context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var wantsToActivateNIP24 by remember {
|
var wantsToActivateNIP24 by remember {
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class Nip96Test {
|
||||||
|
|
||||||
|
val json = """
|
||||||
|
{
|
||||||
|
"api_url": "https://nostr.build/api/v2/nip96/upload",
|
||||||
|
"download_url": "https://media.nostr.build",
|
||||||
|
"supported_nips": [
|
||||||
|
94,
|
||||||
|
96,
|
||||||
|
98
|
||||||
|
],
|
||||||
|
"tos_url": "https://nostr.build/tos/",
|
||||||
|
"content_types": [
|
||||||
|
"image/*",
|
||||||
|
"video/*",
|
||||||
|
"audio/*"
|
||||||
|
],
|
||||||
|
"plans": {
|
||||||
|
"free": {
|
||||||
|
"name": "Free",
|
||||||
|
"is_nip98_required": true,
|
||||||
|
"url": "https://nostr.build",
|
||||||
|
"max_byte_size": 26214400,
|
||||||
|
"file_expiration": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"media_transformations": {
|
||||||
|
"image": [
|
||||||
|
"resizing",
|
||||||
|
"format_conversion",
|
||||||
|
"compression",
|
||||||
|
"metadata_stripping"
|
||||||
|
],
|
||||||
|
"video": [
|
||||||
|
"resizing",
|
||||||
|
"format_conversion",
|
||||||
|
"compression"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"professional": {
|
||||||
|
"name": "Professional",
|
||||||
|
"is_nip98_required": true,
|
||||||
|
"url": "https://nostr.build/signup/new/",
|
||||||
|
"max_byte_size": 10737418240,
|
||||||
|
"file_expiration": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"media_transformations": {
|
||||||
|
"image": [
|
||||||
|
"resizing",
|
||||||
|
"format_conversion",
|
||||||
|
"compression",
|
||||||
|
"metadata_stripping"
|
||||||
|
],
|
||||||
|
"video": [
|
||||||
|
"resizing",
|
||||||
|
"format_conversion",
|
||||||
|
"compression"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"creator": {
|
||||||
|
"name": "Creator",
|
||||||
|
"is_nip98_required": true,
|
||||||
|
"url": "https://nostr.build/signup/new/",
|
||||||
|
"max_byte_size": 26843545600,
|
||||||
|
"file_expiration": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"media_transformations": {
|
||||||
|
"image": [
|
||||||
|
"resizing",
|
||||||
|
"format_conversion",
|
||||||
|
"compression",
|
||||||
|
"metadata_stripping"
|
||||||
|
],
|
||||||
|
"video": [
|
||||||
|
"resizing",
|
||||||
|
"format_conversion",
|
||||||
|
"compression"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun parseNostrBuild() {
|
||||||
|
val info = Nip96Retriever().parse(json)
|
||||||
|
|
||||||
|
assertEquals("https://nostr.build/api/v2/nip96/upload", info.apiUrl)
|
||||||
|
assertEquals("https://media.nostr.build", info.downloadUrl)
|
||||||
|
assertEquals(listOf(94, 96, 98), info.supportedNips)
|
||||||
|
assertEquals("https://nostr.build/tos/", info.tosUrl)
|
||||||
|
assertEquals(listOf("image/*", "video/*", "audio/*"), info.contentTypes)
|
||||||
|
|
||||||
|
assertEquals(listOf("creator", "free", "professional"), info.plans.keys.sorted())
|
||||||
|
|
||||||
|
assertEquals("Free", info.plans["free"]?.name)
|
||||||
|
assertEquals(true, info.plans["free"]?.isNip98Required)
|
||||||
|
assertEquals("https://nostr.build", info.plans["free"]?.url)
|
||||||
|
assertEquals(26214400L, info.plans["free"]?.maxByteSize)
|
||||||
|
assertEquals(listOf(0, 0), info.plans["free"]?.fileExpiration)
|
||||||
|
assertEquals(listOf("image", "video"), info.plans["free"]?.mediaTransformations?.keys?.sorted())
|
||||||
|
|
||||||
|
assertEquals(26843545600L, info.plans["creator"]?.maxByteSize)
|
||||||
|
assertEquals(10737418240L, info.plans["professional"]?.maxByteSize)
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,7 @@ class ChannelMessageEvent(
|
|||||||
markAsSensitive: Boolean,
|
markAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long?,
|
zapRaiserAmount: Long?,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<Event>? = null,
|
||||||
onReady: (ChannelMessageEvent) -> Unit
|
onReady: (ChannelMessageEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val tags = mutableListOf(
|
val tags = mutableListOf(
|
||||||
@ -60,6 +61,11 @@ class ChannelMessageEvent(
|
|||||||
geohash?.let {
|
geohash?.let {
|
||||||
tags.addAll(geohashMipMap(it))
|
tags.addAll(geohashMipMap(it))
|
||||||
}
|
}
|
||||||
|
nip94attachments?.let {
|
||||||
|
it.forEach {
|
||||||
|
tags.add(arrayOf("nip94", it.toJson()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signer.sign(createdAt, kind, tags.toTypedArray(), message, onReady)
|
signer.sign(createdAt, kind, tags.toTypedArray(), message, onReady)
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ class ClassifiedsEvent(
|
|||||||
markAsSensitive: Boolean,
|
markAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long?,
|
zapRaiserAmount: Long?,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<Event>? = null,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (ClassifiedsEvent) -> Unit
|
onReady: (ClassifiedsEvent) -> Unit
|
||||||
@ -133,6 +134,11 @@ class ClassifiedsEvent(
|
|||||||
geohash?.let {
|
geohash?.let {
|
||||||
tags.addAll(geohashMipMap(it))
|
tags.addAll(geohashMipMap(it))
|
||||||
}
|
}
|
||||||
|
nip94attachments?.let {
|
||||||
|
it.forEach {
|
||||||
|
tags.add(arrayOf("nip94", it.toJson()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signer.sign(createdAt, kind, tags.toTypedArray(), message, onReady)
|
signer.sign(createdAt, kind, tags.toTypedArray(), message, onReady)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@ class FileHeaderEvent(
|
|||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
|
fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
|
||||||
|
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] }
|
||||||
|
|
||||||
fun encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) }
|
fun encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) }
|
||||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||||
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
||||||
@ -43,16 +45,19 @@ class FileHeaderEvent(
|
|||||||
private const val MAGNET_URI = "magnet"
|
private const val MAGNET_URI = "magnet"
|
||||||
private const val TORRENT_INFOHASH = "i"
|
private const val TORRENT_INFOHASH = "i"
|
||||||
private const val BLUR_HASH = "blurhash"
|
private const val BLUR_HASH = "blurhash"
|
||||||
|
private const val ORIGINAL_HASH = "ox"
|
||||||
private const val ALT = "alt"
|
private const val ALT = "alt"
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
url: String,
|
url: String,
|
||||||
|
magnetUri: String? = null,
|
||||||
mimeType: String? = null,
|
mimeType: String? = null,
|
||||||
alt: String? = null,
|
alt: String? = null,
|
||||||
hash: String? = null,
|
hash: String? = null,
|
||||||
size: String? = null,
|
size: String? = null,
|
||||||
dimensions: String? = null,
|
dimensions: String? = null,
|
||||||
blurhash: String? = null,
|
blurhash: String? = null,
|
||||||
|
originalHash: String? = null,
|
||||||
magnetURI: String? = null,
|
magnetURI: String? = null,
|
||||||
torrentInfoHash: String? = null,
|
torrentInfoHash: String? = null,
|
||||||
encryptionKey: AESGCM? = null,
|
encryptionKey: AESGCM? = null,
|
||||||
@ -63,12 +68,14 @@ class FileHeaderEvent(
|
|||||||
) {
|
) {
|
||||||
val tags = listOfNotNull(
|
val tags = listOfNotNull(
|
||||||
arrayOf(URL, url),
|
arrayOf(URL, url),
|
||||||
mimeType?.let { arrayOf(MIME_TYPE, mimeType) },
|
magnetUri?.let { arrayOf(MAGNET_URI, it) },
|
||||||
|
mimeType?.let { arrayOf(MIME_TYPE, it) },
|
||||||
alt?.ifBlank { null }?.let { arrayOf(ALT, it) },
|
alt?.ifBlank { null }?.let { arrayOf(ALT, it) },
|
||||||
hash?.let { arrayOf(HASH, it) },
|
hash?.let { arrayOf(HASH, it) },
|
||||||
size?.let { arrayOf(FILE_SIZE, it) },
|
size?.let { arrayOf(FILE_SIZE, it) },
|
||||||
dimensions?.let { arrayOf(DIMENSION, it) },
|
dimensions?.let { arrayOf(DIMENSION, it) },
|
||||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||||
|
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
|
||||||
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
||||||
|
|
||||||
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
||||||
|
@ -24,14 +24,14 @@ class HTTPAuthorizationEvent(
|
|||||||
fun create(
|
fun create(
|
||||||
url: String,
|
url: String,
|
||||||
method: String,
|
method: String,
|
||||||
body: String? = null,
|
file: ByteArray? = null,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (HTTPAuthorizationEvent) -> Unit
|
onReady: (HTTPAuthorizationEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
var hash = ""
|
var hash = ""
|
||||||
body?.let {
|
file?.let {
|
||||||
hash = CryptoUtils.sha256(it.toByteArray()).toHexKey()
|
hash = CryptoUtils.sha256(file).toHexKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
val tags = listOfNotNull(
|
val tags = listOfNotNull(
|
||||||
|
@ -56,6 +56,7 @@ class LiveActivitiesChatMessageEvent(
|
|||||||
markAsSensitive: Boolean,
|
markAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long?,
|
zapRaiserAmount: Long?,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<Event>? = null,
|
||||||
onReady: (LiveActivitiesChatMessageEvent) -> Unit
|
onReady: (LiveActivitiesChatMessageEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val content = message
|
val content = message
|
||||||
@ -80,6 +81,11 @@ class LiveActivitiesChatMessageEvent(
|
|||||||
geohash?.let {
|
geohash?.let {
|
||||||
tags.addAll(geohashMipMap(it))
|
tags.addAll(geohashMipMap(it))
|
||||||
}
|
}
|
||||||
|
nip94attachments?.let {
|
||||||
|
it.forEach {
|
||||||
|
tags.add(arrayOf("nip94", it.toJson()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signer.sign(createdAt, kind, tags.toTypedArray(), content, onReady)
|
signer.sign(createdAt, kind, tags.toTypedArray(), content, onReady)
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ class PollNoteEvent(
|
|||||||
markAsSensitive: Boolean,
|
markAsSensitive: Boolean,
|
||||||
zapRaiserAmount: Long?,
|
zapRaiserAmount: Long?,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<Event>? = null,
|
||||||
onReady: (PollNoteEvent) -> Unit
|
onReady: (PollNoteEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val tags = mutableListOf<Array<String>>()
|
val tags = mutableListOf<Array<String>>()
|
||||||
@ -99,6 +100,11 @@ class PollNoteEvent(
|
|||||||
geohash?.let {
|
geohash?.let {
|
||||||
tags.addAll(geohashMipMap(it))
|
tags.addAll(geohashMipMap(it))
|
||||||
}
|
}
|
||||||
|
nip94attachments?.let {
|
||||||
|
it.forEach {
|
||||||
|
tags.add(arrayOf("nip94", it.toJson()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signer.sign(createdAt, kind, tags.toTypedArray(), msg, onReady)
|
signer.sign(createdAt, kind, tags.toTypedArray(), msg, onReady)
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ class TextNoteEvent(
|
|||||||
root: String?,
|
root: String?,
|
||||||
directMentions: Set<HexKey>,
|
directMentions: Set<HexKey>,
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
|
nip94attachments: List<Event>? = null,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (TextNoteEvent) -> Unit
|
onReady: (TextNoteEvent) -> Unit
|
||||||
@ -96,6 +97,11 @@ class TextNoteEvent(
|
|||||||
geohash?.let {
|
geohash?.let {
|
||||||
tags.addAll(geohashMipMap(it))
|
tags.addAll(geohashMipMap(it))
|
||||||
}
|
}
|
||||||
|
nip94attachments?.let {
|
||||||
|
it.forEach {
|
||||||
|
tags.add(arrayOf("nip94", it.toJson()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signer.sign(createdAt, kind, tags.toTypedArray(), msg, onReady)
|
signer.sign(createdAt, kind, tags.toTypedArray(), msg, onReady)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user