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 com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.actions.FileServer
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImgurServer
|
||||
import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer
|
||||
import com.vitorpamplona.amethyst.ui.actions.NostrFilesDevServer
|
||||
import com.vitorpamplona.amethyst.ui.actions.NostrImgServer
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.Nip96Retriever
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
|
||||
import com.vitorpamplona.amethyst.ui.actions.Nip96Uploader
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.Base64
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
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=="
|
||||
|
||||
private fun testBase(server: FileServer) {
|
||||
val bytes = Base64.getDecoder().decode(image)
|
||||
val contentTypePng = "image/png"
|
||||
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 countDownLatch = CountDownLatch(1)
|
||||
var url: String? = null
|
||||
var error: String? = null
|
||||
|
||||
ImageUploader(Account(KeyPair())).uploadImage(
|
||||
val result = Nip96Uploader(Account(KeyPair())).uploadImage(
|
||||
inputStream,
|
||||
bytes.size.toLong(),
|
||||
"image/gif",
|
||||
server,
|
||||
onSuccess = { newUrl, contentType ->
|
||||
println("Uploaded $contentType to $url")
|
||||
url = newUrl
|
||||
countDownLatch.countDown()
|
||||
},
|
||||
onError = {
|
||||
println("Failed to Upload")
|
||||
error = it.message
|
||||
countDownLatch.countDown()
|
||||
contentTypePng,
|
||||
alt = null,
|
||||
sensitiveContent = null,
|
||||
serverInfo,
|
||||
onProgress = {
|
||||
}
|
||||
)
|
||||
|
||||
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") == true)
|
||||
Assert.assertTrue(url.startsWith("http"))
|
||||
|
||||
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()
|
||||
fun testImgurUpload() = runBlocking {
|
||||
testBase(ImgurServer())
|
||||
fun testNostrCheck() = runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("nostrcheck.me", "https://nostrcheck.me"))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun testNostrBuildUpload() = runBlocking {
|
||||
testBase(NostrBuildServer())
|
||||
fun testNostrage() = runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("nostrage", "https://nostrage.com"))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun testNostrImgUpload() = runBlocking {
|
||||
testBase(NostrImgServer())
|
||||
fun testSove() = runBlocking {
|
||||
testBase(Nip96MediaServers.ServerName("sove", "https://sove.rent"))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun testNostrFilesDevUpload() = runBlocking {
|
||||
testBase(NostrFilesDevServer())
|
||||
fun testNostrBuild() = runBlocking {
|
||||
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.Nip47URI
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.parseThemeType
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
@ -264,7 +264,7 @@ object LocalPreferences {
|
||||
putString(PrefKeys.ZAP_AMOUNTS, Event.mapper.writeValueAsString(account.zapAmountChoices))
|
||||
putString(PrefKeys.REACTION_CHOICES, Event.mapper.writeValueAsString(account.reactionChoices))
|
||||
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_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList.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.PUBLIC
|
||||
|
||||
val defaultFileServer = getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
|
||||
ServersAvailable.values().firstOrNull() { it.name == serverName }
|
||||
} ?: ServersAvailable.NOSTR_BUILD
|
||||
val defaultFileServer = try {
|
||||
getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
|
||||
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 {
|
||||
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
||||
|
@ -4,8 +4,6 @@ import android.content.res.Resources
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asFlow
|
||||
@ -15,6 +13,7 @@ import androidx.lifecycle.switchMap
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.OptOutFromFilters
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
@ -132,7 +131,7 @@ class Account(
|
||||
var zapAmountChoices: List<Long> = DefaultZapAmounts,
|
||||
var reactionChoices: List<String> = DefaultReactions,
|
||||
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 defaultStoriesFollowList: 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
|
||||
|
||||
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
|
||||
|
||||
FileStorageEvent.create(
|
||||
@ -982,8 +987,8 @@ class Account(
|
||||
size = headerInfo.size.toString(),
|
||||
dimensions = headerInfo.dim,
|
||||
blurhash = headerInfo.blurHash,
|
||||
alt = headerInfo.alt,
|
||||
sensitiveContent = headerInfo.sensitiveContent,
|
||||
alt = alt,
|
||||
sensitiveContent = sensitiveContent,
|
||||
signer = signer
|
||||
) { signedEvent ->
|
||||
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
|
||||
|
||||
Client.send(data, relayList = relayList)
|
||||
@ -1005,7 +1010,19 @@ class Account(
|
||||
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)
|
||||
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
|
||||
|
||||
FileHeaderEvent.create(
|
||||
url = headerInfo.url,
|
||||
url = imageUrl,
|
||||
magnetUri = magnetUri,
|
||||
mimeType = headerInfo.mimeType,
|
||||
hash = headerInfo.hash,
|
||||
size = headerInfo.size.toString(),
|
||||
dimensions = headerInfo.dim,
|
||||
blurhash = headerInfo.blurHash,
|
||||
alt = headerInfo.alt,
|
||||
sensitiveContent = headerInfo.sensitiveContent,
|
||||
alt = alt,
|
||||
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
|
||||
) { event ->
|
||||
sendHeader(event, relayList = relayList, onReady)
|
||||
@ -1046,7 +1102,8 @@ class Account(
|
||||
wantsToMarkAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long? = null,
|
||||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1072,6 +1129,7 @@ class Account(
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
directMentions = directMentions,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
@ -1102,7 +1160,8 @@ class Account(
|
||||
root: String?,
|
||||
directMentions: Set<HexKey>,
|
||||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1123,6 +1182,7 @@ class Account(
|
||||
root = root,
|
||||
directMentions = directMentions,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
@ -1160,7 +1220,8 @@ class Account(
|
||||
wantsToMarkAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long? = null,
|
||||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@ -1182,7 +1243,8 @@ class Account(
|
||||
zapReceiver = zapReceiver,
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
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
|
||||
|
||||
val repliesToHex = replyTo?.map { it.idHex }
|
||||
@ -1216,6 +1278,7 @@ class Account(
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer
|
||||
) {
|
||||
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
|
||||
|
||||
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
|
||||
@ -1239,6 +1302,7 @@ class Account(
|
||||
markAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = zapRaiserAmount,
|
||||
geohash = geohash,
|
||||
nip94attachments = nip94attachments,
|
||||
signer = signer
|
||||
) {
|
||||
Client.send(it)
|
||||
@ -1674,7 +1738,7 @@ class Account(
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun changeDefaultFileServer(server: ServersAvailable) {
|
||||
fun changeDefaultFileServer(server: Nip96MediaServers.ServerName) {
|
||||
defaultFileServer = server
|
||||
live.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
|
||||
|
||||
class FileHeader(
|
||||
val url: String,
|
||||
val mimeType: String?,
|
||||
val hash: String,
|
||||
val size: Int,
|
||||
val dim: String?,
|
||||
val blurHash: String?,
|
||||
val alt: String? = null,
|
||||
val sensitiveContent: Boolean = false
|
||||
val blurHash: String?
|
||||
) {
|
||||
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 {
|
||||
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(fileUrl)
|
||||
|
||||
if (imageData != null) {
|
||||
prepare(imageData, fileUrl, mimeType, alt, sensitiveContent, onReady, onError)
|
||||
prepare(imageData, mimeType, dimPrecomputed, onReady, onError)
|
||||
} else {
|
||||
onError()
|
||||
}
|
||||
@ -37,10 +40,8 @@ class FileHeader(
|
||||
|
||||
fun prepare(
|
||||
data: ByteArray,
|
||||
fileUrl: String,
|
||||
mimeType: String?,
|
||||
alt: String?,
|
||||
sensitiveContent: Boolean,
|
||||
dimPrecomputed: String?,
|
||||
onReady: (FileHeader) -> Unit,
|
||||
onError: () -> Unit
|
||||
) {
|
||||
@ -76,10 +77,10 @@ class FileHeader(
|
||||
Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4), dim)
|
||||
}
|
||||
} 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) {
|
||||
Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}")
|
||||
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.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -19,6 +19,11 @@ import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ServerOption(
|
||||
val server: Nip96MediaServers.ServerName,
|
||||
val isNip95: Boolean
|
||||
)
|
||||
|
||||
@Stable
|
||||
open class NewMediaModel : ViewModel() {
|
||||
var account: Account? = null
|
||||
@ -27,7 +32,7 @@ open class NewMediaModel : ViewModel() {
|
||||
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
var mediaType by mutableStateOf<String?>(null)
|
||||
|
||||
var selectedServer by mutableStateOf<ServersAvailable?>(null)
|
||||
var selectedServer by mutableStateOf<ServerOption?>(null)
|
||||
var alt by mutableStateOf("")
|
||||
var sensitiveContent by mutableStateOf(false)
|
||||
|
||||
@ -43,20 +48,7 @@ open class NewMediaModel : ViewModel() {
|
||||
this.account = account
|
||||
this.galleryUri = uri
|
||||
this.mediaType = contentType
|
||||
this.selectedServer = defaultServer()
|
||||
|
||||
// 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
|
||||
}
|
||||
this.selectedServer = ServerOption(defaultServer(), false)
|
||||
}
|
||||
|
||||
fun upload(context: Context, relayList: List<Relay>? = null) {
|
||||
@ -76,8 +68,7 @@ open class NewMediaModel : ViewModel() {
|
||||
contentType,
|
||||
context.applicationContext,
|
||||
onReady = { fileUri, contentType, size ->
|
||||
|
||||
if (selectedServer == ServersAvailable.NIP95) {
|
||||
if (serverToUse.isNip95) {
|
||||
uploadingPercentage.value = 0.2f
|
||||
uploadingDescription.value = "Loading"
|
||||
contentResolver.openInputStream(fileUri)?.use {
|
||||
@ -95,30 +86,35 @@ open class NewMediaModel : ViewModel() {
|
||||
uploadingPercentage.value = 0.2f
|
||||
uploadingDescription.value = "Uploading"
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = serverToUse,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
createNIP94Record(
|
||||
imageUrl,
|
||||
mimeType,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
relayList = relayList
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
try {
|
||||
val result = Nip96Uploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
alt = alt,
|
||||
sensitiveContent = if (sensitiveContent) "" else null,
|
||||
server = serverToUse.server,
|
||||
contentResolver = contentResolver,
|
||||
onProgress = { percent: Float ->
|
||||
uploadingPercentage.value = 0.2f + (0.2f * percent)
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
alt = ""
|
||||
selectedServer = account?.defaultFileServer
|
||||
selectedServer = ServerOption(defaultServer(), false)
|
||||
}
|
||||
|
||||
fun canPost(): Boolean {
|
||||
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
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
uploadingDescription.value = "Server Processing"
|
||||
// Images don't seem to be ready immediately after upload
|
||||
uploadingDescription.value = "Server Processing"
|
||||
// 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"
|
||||
uploadingPercentage.value = 0.60f
|
||||
if (imageUrl.isNullOrBlank()) {
|
||||
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) {
|
||||
uploadingPercentage.value = 0.80f
|
||||
uploadingDescription.value = "Hashing"
|
||||
uploadingDescription.value = "Downloading"
|
||||
uploadingPercentage.value = 0.60f
|
||||
|
||||
FileHeader.prepare(
|
||||
imageData,
|
||||
imageUrl,
|
||||
mimeType,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
uploadingPercentage.value = 0.90f
|
||||
uploadingDescription.value = "Sending"
|
||||
account?.sendHeader(it, relayList) {
|
||||
uploadingPercentage.value = 1.00f
|
||||
isUploadingImage = false
|
||||
onceUploaded()
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
cancel()
|
||||
uploadingPercentage.value = 0.00f
|
||||
uploadingDescription.value = null
|
||||
val imageData: ByteArray? = ImageDownloader().waitAndGetImage(imageUrl)
|
||||
|
||||
if (imageData != null) {
|
||||
uploadingPercentage.value = 0.80f
|
||||
uploadingDescription.value = "Hashing"
|
||||
|
||||
FileHeader.prepare(
|
||||
data = imageData,
|
||||
mimeType = remoteMimeType ?: localContentType,
|
||||
dimPrecomputed = dim,
|
||||
onReady = {
|
||||
uploadingPercentage.value = 0.90f
|
||||
uploadingDescription.value = "Sending"
|
||||
account?.sendHeader(imageUrl, magnet, it, alt, sensitiveContent, originalHash, relayList) {
|
||||
uploadingPercentage.value = 1.00f
|
||||
isUploadingImage = false
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
onceUploaded()
|
||||
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) {
|
||||
FileHeader.prepare(
|
||||
bytes,
|
||||
"",
|
||||
mimeType,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
null,
|
||||
onReady = {
|
||||
uploadingDescription.value = "Signing"
|
||||
uploadingPercentage.value = 0.40f
|
||||
account?.createNip95(bytes, headerInfo = it) { nip95 ->
|
||||
account?.createNip95(bytes, headerInfo = it, alt, sensitiveContent) { nip95 ->
|
||||
uploadingDescription.value = "Sending"
|
||||
uploadingPercentage.value = 0.60f
|
||||
account?.sendNip95(nip95.first, nip95.second, relayList)
|
||||
account?.consumeAndSendNip95(nip95.first, nip95.second, relayList)
|
||||
|
||||
uploadingPercentage.value = 1.00f
|
||||
isUploadingImage = false
|
||||
@ -243,7 +257,7 @@ open class NewMediaModel : ViewModel() {
|
||||
|
||||
fun isImage() = mediaType?.startsWith("image")
|
||||
fun isVideo() = mediaType?.startsWith("video")
|
||||
fun defaultServer() = account?.defaultFileServer
|
||||
fun defaultServer() = account?.defaultFileServer ?: Nip96MediaServers.DEFAULT[0]
|
||||
fun onceUploaded(onceUploaded: () -> Unit) {
|
||||
this.onceUploaded = onceUploaded
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil.compose.AsyncImage
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
@ -146,7 +146,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
||||
onPost = {
|
||||
onClose()
|
||||
postViewModel.upload(context, relayList)
|
||||
postViewModel.selectedServer?.let { account.changeDefaultFileServer(it) }
|
||||
postViewModel.selectedServer?.let { account.changeDefaultFileServer(it.server) }
|
||||
},
|
||||
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
|
||||
fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewModel) {
|
||||
val fileServers = listOf(
|
||||
// Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)),
|
||||
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 fileServers = Nip96MediaServers.DEFAULT.map { ServerOption(it, false) } + listOf(
|
||||
ServerOption(
|
||||
Nip96MediaServers.ServerName(
|
||||
"NIP95",
|
||||
stringResource(id = R.string.upload_server_relays_nip95)
|
||||
),
|
||||
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
|
||||
|
||||
Row(
|
||||
@ -252,10 +245,12 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
) {
|
||||
TextSpinner(
|
||||
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,
|
||||
onSelect = {
|
||||
postViewModel.selectedServer = fileServers[it].first
|
||||
postViewModel.selectedServer = fileServers[it]
|
||||
},
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
@ -263,44 +258,40 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
||||
)
|
||||
}
|
||||
|
||||
if (isNIP94Server(postViewModel.selectedServer) ||
|
||||
postViewModel.selectedServer == ServersAvailable.NIP95
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SettingSwitchItem(
|
||||
checked = postViewModel.sensitiveContent,
|
||||
onCheckedChange = { postViewModel.sensitiveContent = it },
|
||||
title = R.string.add_sensitive_content_label,
|
||||
description = R.string.add_sensitive_content_description
|
||||
)
|
||||
}
|
||||
SettingSwitchItem(
|
||||
checked = postViewModel.sensitiveContent,
|
||||
onCheckedChange = { postViewModel.sensitiveContent = it },
|
||||
title = R.string.add_sensitive_content_label,
|
||||
description = R.string.add_sensitive_content_description
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
Row(
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringResource(R.string.content_description)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
||||
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
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
||||
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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -112,8 +112,8 @@ import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
||||
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
|
||||
@ -404,8 +404,8 @@ fun NewPostView(
|
||||
url,
|
||||
accountViewModel.account.defaultFileServer,
|
||||
onAdd = { alt, server, sensitiveContent ->
|
||||
postViewModel.upload(url, alt, sensitiveContent, server, context, relayList)
|
||||
accountViewModel.account.changeDefaultFileServer(server)
|
||||
postViewModel.upload(url, alt, sensitiveContent, false, server, context, relayList)
|
||||
accountViewModel.account.changeDefaultFileServer(server.server)
|
||||
},
|
||||
onCancel = {
|
||||
postViewModel.contentToAddUrl = null
|
||||
@ -1597,8 +1597,8 @@ fun CreateButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
||||
@Composable
|
||||
fun ImageVideoDescription(
|
||||
uri: Uri,
|
||||
defaultServer: ServersAvailable,
|
||||
onAdd: (String, ServersAvailable, Boolean) -> Unit,
|
||||
defaultServer: Nip96MediaServers.ServerName,
|
||||
onAdd: (String, ServerOption, Boolean) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
@ -1609,23 +1609,19 @@ fun ImageVideoDescription(
|
||||
val isImage = mediaType.startsWith("image")
|
||||
val isVideo = mediaType.startsWith("video")
|
||||
|
||||
val fileServers = listOf(
|
||||
// Triple(ServersAvailable.IMGUR, stringResource(id = R.string.upload_server_imgur), stringResource(id = R.string.upload_server_imgur_explainer)),
|
||||
Triple(ServersAvailable.NOSTRIMG, stringResource(id = R.string.upload_server_nostrimg), stringResource(id = R.string.upload_server_nostrimg_explainer)),
|
||||
Triple(ServersAvailable.NOSTR_BUILD, stringResource(id = R.string.upload_server_nostrbuild), stringResource(id = R.string.upload_server_nostrbuild_explainer)),
|
||||
Triple(ServersAvailable.NOSTRFILES_DEV, stringResource(id = R.string.upload_server_nostrfilesdev), stringResource(id = R.string.upload_server_nostrfilesdev_explainer)),
|
||||
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)),
|
||||
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 fileServers = Nip96MediaServers.DEFAULT.map { ServerOption(it, false) } + listOf(
|
||||
ServerOption(
|
||||
Nip96MediaServers.ServerName(
|
||||
"NIP95",
|
||||
stringResource(id = R.string.upload_server_relays_nip95)
|
||||
),
|
||||
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() }
|
||||
|
||||
var selectedServer by remember { mutableStateOf(defaultServer) }
|
||||
var selectedServer by remember { mutableStateOf(ServerOption(defaultServer, false)) }
|
||||
var message by remember { mutableStateOf("") }
|
||||
var sensitiveContent by remember { mutableStateOf(false) }
|
||||
|
||||
@ -1735,10 +1731,12 @@ fun ImageVideoDescription(
|
||||
) {
|
||||
TextSpinner(
|
||||
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,
|
||||
onSelect = {
|
||||
selectedServer = fileServers[it].first
|
||||
selectedServer = fileServers[it]
|
||||
},
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
@ -1746,45 +1744,41 @@ fun ImageVideoDescription(
|
||||
)
|
||||
}
|
||||
|
||||
if (isNIP94Server(selectedServer) ||
|
||||
selectedServer == ServersAvailable.NIP95
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SettingSwitchItem(
|
||||
checked = sensitiveContent,
|
||||
onCheckedChange = { sensitiveContent = it },
|
||||
title = R.string.add_sensitive_content_label,
|
||||
description = R.string.add_sensitive_content_description
|
||||
)
|
||||
}
|
||||
SettingSwitchItem(
|
||||
checked = sensitiveContent,
|
||||
onCheckedChange = { sensitiveContent = it },
|
||||
title = R.string.add_sensitive_content_label,
|
||||
description = R.string.add_sensitive_content_description
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
Row(
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringResource(R.string.content_description)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
||||
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
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)),
|
||||
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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
|
@ -18,7 +18,6 @@ import com.fonfon.kgeohash.toGeoHash
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.FileHeader
|
||||
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.ClassifiedsEvent
|
||||
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.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.events.findURLs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -63,6 +66,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
var mentions by mutableStateOf<List<User>?>(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 urlPreview by mutableStateOf<String?>(null)
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
@ -219,11 +225,25 @@ open class NewPostViewModel() : ViewModel() {
|
||||
|
||||
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 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 {
|
||||
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) {
|
||||
account?.sendPrivateMessage(tagger.message, originalNote!!.author!!, originalNote!!, tagger.mentions, zapReceiver, wantsToMarkAsSensitive, localZapRaiserAmount, geoHash)
|
||||
@ -281,7 +301,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
wantsToMarkAsSensitive,
|
||||
localZapRaiserAmount,
|
||||
relayList,
|
||||
geoHash
|
||||
geoHash,
|
||||
nip94attachments = usedAttachments
|
||||
)
|
||||
} else if (wantsProduct) {
|
||||
account?.sendClassifieds(
|
||||
@ -298,7 +319,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||
zapRaiserAmount = localZapRaiserAmount,
|
||||
relayList = relayList,
|
||||
geohash = geoHash
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments
|
||||
)
|
||||
} else {
|
||||
// adds markers
|
||||
@ -322,7 +344,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
root = rootId,
|
||||
directMentions = tagger.directMentions,
|
||||
relayList = relayList,
|
||||
geohash = geoHash
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -330,7 +353,15 @@ open class NewPostViewModel() : ViewModel() {
|
||||
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
|
||||
contentToAddUrl = null
|
||||
|
||||
@ -343,35 +374,49 @@ open class NewPostViewModel() : ViewModel() {
|
||||
contentType,
|
||||
context.applicationContext,
|
||||
onReady = { fileUri, contentType, size ->
|
||||
if (server == ServersAvailable.NIP95) {
|
||||
if (server.isNip95) {
|
||||
contentResolver.openInputStream(fileUri)?.use {
|
||||
createNIP95Record(it.readBytes(), contentType, alt, sensitiveContent, relayList = relayList)
|
||||
}
|
||||
} else {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = server,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
if (isNIP94Server(server)) {
|
||||
createNIP94Record(imageUrl, mimeType, alt, sensitiveContent)
|
||||
} else {
|
||||
isUploadingImage = false
|
||||
message = TextFieldValue(message.text + "\n" + imageUrl)
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
Log.e("ImageUploader", "Failed to upload the image / video", it)
|
||||
try {
|
||||
val result = Nip96Uploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
alt = alt,
|
||||
sensitiveContent = if (sensitiveContent) "" else null,
|
||||
server = server.server,
|
||||
contentResolver = contentResolver,
|
||||
onProgress = { }
|
||||
)
|
||||
|
||||
if (!isPrivate) {
|
||||
createNIP94Record(
|
||||
uploadingResult = result,
|
||||
localContentType = contentType,
|
||||
alt = alt,
|
||||
sensitiveContent = sensitiveContent
|
||||
)
|
||||
} else {
|
||||
val url = result.tags?.firstOrNull() { it.size > 1 && it[0] == "url" }?.get(1)
|
||||
|
||||
isUploadingImage = false
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
message = TextFieldValue(message.text + "\n" + url)
|
||||
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) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Images don't seem to be ready immediately after upload
|
||||
FileHeader.prepare(
|
||||
imageUrl,
|
||||
mimeType,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
onReady = {
|
||||
account?.sendHeader(it, relayList = relayList) { note ->
|
||||
isUploadingImage = false
|
||||
suspend fun createNIP94Record(
|
||||
uploadingResult: Nip96Uploader.PartialEvent,
|
||||
localContentType: String?,
|
||||
alt: String?,
|
||||
sensitiveContent: Boolean
|
||||
) {
|
||||
// Images don't seem to be ready immediately after upload
|
||||
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 }
|
||||
|
||||
message = TextFieldValue(message.text + "\nnostr:" + note.toNEvent())
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
}
|
||||
)
|
||||
if (imageUrl.isNullOrBlank()) {
|
||||
Log.e("ImageDownload", "Couldn't download image from server")
|
||||
cancel()
|
||||
isUploadingImage = false
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
FileHeader.prepare(
|
||||
bytes,
|
||||
"",
|
||||
mimeType,
|
||||
alt,
|
||||
sensitiveContent,
|
||||
null,
|
||||
onReady = {
|
||||
account?.createNip95(bytes, headerInfo = it) { nip95 ->
|
||||
val note = nip95.let { it1 -> account?.sendNip95(it1.first, it1.second, relayList = relayList) }
|
||||
account?.createNip95(bytes, headerInfo = it, alt, sensitiveContent) { nip95 ->
|
||||
nip95attachments = nip95attachments + nip95
|
||||
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
isUploadingImage = false
|
||||
|
||||
|
@ -184,23 +184,35 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
context.applicationContext,
|
||||
onReady = { fileUri, contentType, size ->
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
ImageUploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
server = account.defaultFileServer,
|
||||
contentResolver = contentResolver,
|
||||
onSuccess = { imageUrl, mimeType ->
|
||||
try {
|
||||
val result = Nip96Uploader(account).uploadImage(
|
||||
uri = fileUri,
|
||||
contentType = contentType,
|
||||
size = size,
|
||||
alt = null,
|
||||
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)
|
||||
onUploaded(imageUrl)
|
||||
},
|
||||
onError = {
|
||||
onUploaded(url)
|
||||
} else {
|
||||
onUploading(false)
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
onUploading(false)
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image / video")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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.Note
|
||||
import com.vitorpamplona.amethyst.model.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
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.components.LoadNote
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
@ -401,24 +401,13 @@ fun EditFieldRow(
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
modifier = EditFieldLeadingIconModifier
|
||||
) {
|
||||
val fileServer = if (isPrivate) {
|
||||
// TODO: Make private servers
|
||||
when (accountViewModel.account.defaultFileServer) {
|
||||
ServersAvailable.NOSTR_BUILD -> ServersAvailable.NOSTR_BUILD
|
||||
ServersAvailable.NOSTRIMG -> ServersAvailable.NOSTRIMG
|
||||
ServersAvailable.NOSTRFILES_DEV -> ServersAvailable.NOSTRFILES_DEV
|
||||
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)
|
||||
channelScreenModel.upload(
|
||||
galleryUri = it,
|
||||
alt = null,
|
||||
sensitiveContent = false,
|
||||
server = ServerOption(accountViewModel.account.defaultFileServer, false),
|
||||
context = context
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
|
@ -65,12 +65,12 @@ import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
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.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
@ -379,26 +379,14 @@ fun PrivateMessageEditFieldRow(
|
||||
.size(30.dp)
|
||||
.padding(start = 2.dp)
|
||||
) {
|
||||
val fileServer = if (isPrivate) {
|
||||
// TODO: Make private servers
|
||||
when (accountViewModel.account.defaultFileServer) {
|
||||
ServersAvailable.NOSTR_BUILD -> ServersAvailable.NOSTR_BUILD
|
||||
ServersAvailable.NOSTRIMG -> ServersAvailable.NOSTRIMG
|
||||
ServersAvailable.NOSTRFILES_DEV -> ServersAvailable.NOSTRFILES_DEV
|
||||
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)
|
||||
channelScreenModel.upload(
|
||||
galleryUri = it,
|
||||
alt = null,
|
||||
sensitiveContent = false,
|
||||
isPrivate = isPrivate,
|
||||
server = ServerOption(accountViewModel.account.defaultFileServer, false),
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null,
|
||||
onReady: (ChannelMessageEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf(
|
||||
@ -60,6 +61,11 @@ class ChannelMessageEvent(
|
||||
geohash?.let {
|
||||
tags.addAll(geohashMipMap(it))
|
||||
}
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
tags.add(arrayOf("nip94", it.toJson()))
|
||||
}
|
||||
}
|
||||
|
||||
signer.sign(createdAt, kind, tags.toTypedArray(), message, onReady)
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ class ClassifiedsEvent(
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ClassifiedsEvent) -> Unit
|
||||
@ -133,6 +134,11 @@ class ClassifiedsEvent(
|
||||
geohash?.let {
|
||||
tags.addAll(geohashMipMap(it))
|
||||
}
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
tags.add(arrayOf("nip94", it.toJson()))
|
||||
}
|
||||
}
|
||||
|
||||
signer.sign(createdAt, kind, tags.toTypedArray(), message, onReady)
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ class FileHeaderEvent(
|
||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
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 mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.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 TORRENT_INFOHASH = "i"
|
||||
private const val BLUR_HASH = "blurhash"
|
||||
private const val ORIGINAL_HASH = "ox"
|
||||
private const val ALT = "alt"
|
||||
|
||||
fun create(
|
||||
url: String,
|
||||
magnetUri: String? = null,
|
||||
mimeType: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
blurhash: String? = null,
|
||||
originalHash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
@ -63,12 +68,14 @@ class FileHeaderEvent(
|
||||
) {
|
||||
val tags = listOfNotNull(
|
||||
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) },
|
||||
hash?.let { arrayOf(HASH, it) },
|
||||
size?.let { arrayOf(FILE_SIZE, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it) },
|
||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
|
||||
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
||||
|
||||
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
||||
|
@ -24,14 +24,14 @@ class HTTPAuthorizationEvent(
|
||||
fun create(
|
||||
url: String,
|
||||
method: String,
|
||||
body: String? = null,
|
||||
file: ByteArray? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (HTTPAuthorizationEvent) -> Unit
|
||||
) {
|
||||
var hash = ""
|
||||
body?.let {
|
||||
hash = CryptoUtils.sha256(it.toByteArray()).toHexKey()
|
||||
file?.let {
|
||||
hash = CryptoUtils.sha256(file).toHexKey()
|
||||
}
|
||||
|
||||
val tags = listOfNotNull(
|
||||
|
@ -56,6 +56,7 @@ class LiveActivitiesChatMessageEvent(
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null,
|
||||
onReady: (LiveActivitiesChatMessageEvent) -> Unit
|
||||
) {
|
||||
val content = message
|
||||
@ -80,6 +81,11 @@ class LiveActivitiesChatMessageEvent(
|
||||
geohash?.let {
|
||||
tags.addAll(geohashMipMap(it))
|
||||
}
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
tags.add(arrayOf("nip94", it.toJson()))
|
||||
}
|
||||
}
|
||||
|
||||
signer.sign(createdAt, kind, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ class PollNoteEvent(
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null,
|
||||
onReady: (PollNoteEvent) -> Unit
|
||||
) {
|
||||
val tags = mutableListOf<Array<String>>()
|
||||
@ -99,6 +100,11 @@ class PollNoteEvent(
|
||||
geohash?.let {
|
||||
tags.addAll(geohashMipMap(it))
|
||||
}
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
tags.add(arrayOf("nip94", it.toJson()))
|
||||
}
|
||||
}
|
||||
|
||||
signer.sign(createdAt, kind, tags.toTypedArray(), msg, onReady)
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ class TextNoteEvent(
|
||||
root: String?,
|
||||
directMentions: Set<HexKey>,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<Event>? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (TextNoteEvent) -> Unit
|
||||
@ -96,6 +97,11 @@ class TextNoteEvent(
|
||||
geohash?.let {
|
||||
tags.addAll(geohashMipMap(it))
|
||||
}
|
||||
nip94attachments?.let {
|
||||
it.forEach {
|
||||
tags.add(arrayOf("nip94", it.toJson()))
|
||||
}
|
||||
}
|
||||
|
||||
signer.sign(createdAt, kind, tags.toTypedArray(), msg, onReady)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user