Skip to content

Commit

Permalink
Adds support for Blossom media servers.
Browse files Browse the repository at this point in the history
  • Loading branch information
vitorpamplona committed Nov 21, 2024
1 parent c89c5eb commit 2c9e2de
Show file tree
Hide file tree
Showing 33 changed files with 1,336 additions and 430 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,17 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.service.BlossomUploader
import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.Nip96Retriever
import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.toHexKey
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.fail
import kotlinx.coroutines.CoroutineScope
Expand All @@ -47,36 +52,83 @@ import kotlin.random.Random

@RunWith(AndroidJUnit4::class)
class ImageUploadTesting {
private suspend fun testBase(server: Nip96MediaServers.ServerName) {
val serverInfo =
Nip96Retriever()
.loadInfo(
server.baseUrl,
false,
)
val account =
Account(
AccountSettings(KeyPair()),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
)

private suspend fun getBitmap(): ByteArray {
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
for (x in 0 until bitmap.width) {
for (y in 0 until bitmap.height) {
bitmap.setPixel(x, y, Color.rgb(Random.nextInt(), Random.nextInt(), Random.nextInt()))
}
}

val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos)
val bytes = baos.toByteArray()
val inputStream = bytes.inputStream()
return baos.toByteArray()
}

val account =
Account(
AccountSettings(KeyPair()),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
)
private suspend fun testBase(server: ServerName) {
if (server.type == ServerType.NIP96) {
testNip96(server)
} else {
testBlossom(server)
}
}

private suspend fun testBlossom(server: ServerName) {
val paylod = getBitmap()
val initialHash = CryptoUtils.sha256(paylod).toHexKey()
val inputStream = paylod.inputStream()
val result =
BlossomUploader(account)
.uploadImage(
inputStream,
initialHash,
paylod.size,
"filename.png",
"image/png",
alt = null,
sensitiveContent = null,
server,
forceProxy = { false },
context = InstrumentationRegistry.getInstrumentation().targetContext,
)

assertEquals("image/png", result.type)
assertEquals(paylod.size.toLong(), result.size)
assertEquals(initialHash, result.sha256)
assertEquals("${server.baseUrl}/$initialHash", result.url)

val imageData: ByteArray =
ImageDownloader().waitAndGetImage(result.url!!, false)
?: run {
fail("${server.name}: Should not be null")
return
}

val downloadedHash = CryptoUtils.sha256(imageData).toHexKey()
assertEquals(initialHash, downloadedHash)
}

private suspend fun testNip96(server: ServerName) {
val serverInfo =
Nip96Retriever()
.loadInfo(
server.baseUrl,
false,
)

val paylod = getBitmap()
val inputStream = paylod.inputStream()
val result =
Nip96Uploader(account)
.uploadImage(
inputStream,
bytes.size.toLong(),
paylod.size.toLong(),
"image/png",
alt = null,
sensitiveContent = null,
Expand Down Expand Up @@ -140,66 +192,84 @@ class ImageUploadTesting {
@Test
fun runTestOnDefaultServers() =
runBlocking {
Nip96MediaServers.DEFAULT.forEach {
DEFAULT_MEDIA_SERVERS.forEach {
testBase(it)
}
}

@Test()
fun testNostrCheck() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostrcheck.me", "https://nostrcheck.me"))
testBase(ServerName("nostrcheck.me", "https://nostrcheck.me", ServerType.NIP96))
}

@Test()
@Ignore("Not Working anymore")
fun testNostrage() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostrage", "https://nostrage.com"))
testBase(ServerName("nostrage", "https://nostrage.com", ServerType.NIP96))
}

@Test()
@Ignore("Not Working anymore")
fun testSove() =
runBlocking {
testBase(Nip96MediaServers.ServerName("sove", "https://sove.rent"))
testBase(ServerName("sove", "https://sove.rent", ServerType.NIP96))
}

@Test()
fun testNostrBuild() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostr.build", "https://nostr.build"))
testBase(ServerName("nostr.build", "https://nostr.build", ServerType.NIP96))
}

@Test()
@Ignore("Not Working anymore")
fun testSovbit() =
runBlocking {
testBase(Nip96MediaServers.ServerName("sovbit", "https://files.sovbit.host"))
testBase(ServerName("sovbit", "https://files.sovbit.host", ServerType.NIP96))
}

@Test()
fun testVoidCat() =
runBlocking {
testBase(Nip96MediaServers.ServerName("void.cat", "https://void.cat"))
testBase(ServerName("void.cat", "https://void.cat", ServerType.NIP96))
}

@Test()
fun testNostrPic() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostpic.com", "https://nostpic.com"))
testBase(ServerName("nostpic.com", "https://nostpic.com", ServerType.NIP96))
}

@Test(expected = RuntimeException::class)
fun testSprovoostNl() =
runBlocking {
testBase(Nip96MediaServers.ServerName("sprovoost.nl", "https://img.sprovoost.nl/"))
testBase(ServerName("sprovoost.nl", "https://img.sprovoost.nl/", ServerType.NIP96))
}

@Test()
@Ignore("Not Working anymore")
fun testNostrOnch() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostr.onch.services", "https://nostr.onch.services"))
testBase(ServerName("nostr.onch.services", "https://nostr.onch.services", ServerType.NIP96))
}

@Ignore("Changes sha256")
fun testPrimalBlossom() =
runBlocking {
testBase(ServerName("primal.net", "https://blossom.primal.net", ServerType.Blossom))
}

@Test()
fun testNostrCheckBlossom() =
runBlocking {
testBase(ServerName("nostrcheck", "https://cdn.nostrcheck.me", ServerType.Blossom))
}

@Ignore("Requires Payment")
fun testSatelliteBlossom() =
runBlocking {
testBase(ServerName("satellite", "https://cdn.satellite.earth", ServerType.Blossom))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ import com.vitorpamplona.amethyst.model.DefaultZapAmounts
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
import com.vitorpamplona.amethyst.model.Settings
import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType
Expand Down Expand Up @@ -521,7 +522,7 @@ object LocalPreferences {
val localRelays = parseOrNull<Set<RelaySetupInfo>>(PrefKeys.RELAYS) ?: emptySet()

val zapPaymentRequestServer = parseOrNull<Nip47WalletConnect.Nip47URI>(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER)
val defaultFileServer = parseOrNull<Nip96MediaServers.ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: Nip96MediaServers.DEFAULT[0]
val defaultFileServer = parseOrNull<ServerName>(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0]

val pendingAttestations = parseOrNull<Map<HexKey, String>>(PrefKeys.PENDING_ATTESTATIONS) ?: mapOf()
val localRelayServers = getStringSet(PrefKeys.LOCAL_RELAY_SERVERS, null) ?: setOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import coil3.util.DebugLogger
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.Base64Fetcher
import com.vitorpamplona.amethyst.service.BlurHashFetcher
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
Expand Down Expand Up @@ -134,6 +135,9 @@ class ServiceManager(
}
add(SvgDecoder.Factory())
add(Base64Fetcher.Factory)
add(BlurHashFetcher.Factory)
add(Base64Fetcher.BKeyer)
add(BlurHashFetcher.BKeyer)
add(
OkHttpNetworkFetcherFactory(
callFactory = {
Expand Down
89 changes: 89 additions & 0 deletions amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.tryAndWait
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.Constants
Expand All @@ -57,6 +60,8 @@ import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.encoders.hexToByteArray
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.AppSpecificDataEvent
import com.vitorpamplona.quartz.events.BlossomAuthorizationEvent
import com.vitorpamplona.quartz.events.BlossomServersEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
Expand Down Expand Up @@ -138,6 +143,7 @@ import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.czeal.rfc3986.URIReference
import java.math.BigDecimal
import java.util.Locale
import java.util.UUID
Expand Down Expand Up @@ -628,6 +634,19 @@ class Account(
)
}

val liveServerList: StateFlow<List<ServerName>> by lazy {
combine(getFileServersListFlow(), getBlossomServersListFlow()) { nip96, blossom ->
mergeServerList(nip96.note.event as? FileServersEvent, blossom.note.event as? BlossomServersEvent)
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
runBlocking {
mergeServerList(getFileServersList(), getBlossomServersList())
},
)
}

suspend fun loadAndCombineFlows(listName: String): LiveFollowList? {
val flows = loadFlowsFor(listName)
return mapIntoFollowLists(
Expand Down Expand Up @@ -1482,6 +1501,26 @@ class Account(
HTTPAuthorizationEvent.create(url, method, body, signer, onReady = onReady)
}

fun createBlossomUploadAuth(
hash: HexKey,
alt: String,
onReady: (BlossomAuthorizationEvent) -> Unit,
) {
if (!isWriteable()) return

BlossomAuthorizationEvent.createUploadAuth(hash, alt, signer, onReady = onReady)
}

fun createBlossomDeleteAuth(
hash: HexKey,
alt: String,
onReady: (BlossomAuthorizationEvent) -> Unit,
) {
if (!isWriteable()) return

BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer, onReady = onReady)
}

suspend fun boost(note: Note) {
if (!isWriteable()) return

Expand Down Expand Up @@ -3663,6 +3702,31 @@ class Account(

fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddressATag(userProfile().pubkeyHex))

fun getBlossomServersList(): BlossomServersEvent? = getBlossomServersNote().event as? BlossomServersEvent

fun getBlossomServersListFlow(): StateFlow<NoteState> = getBlossomServersNote().flow().metadata.stateFlow

fun getBlossomServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(BlossomServersEvent.createAddressATag(userProfile().pubkeyHex))

fun host(url: String): String =
try {
URIReference.parse(url).host.value
} catch (e: Exception) {
url
}

fun mergeServerList(
nip96: FileServersEvent?,
blossom: BlossomServersEvent?,
): List<ServerName> {
val nip96servers = nip96?.servers()?.map { ServerName(host(it), it, ServerType.NIP96) } ?: emptyList()
val blossomServers = blossom?.servers()?.map { ServerName(host(it), it, ServerType.Blossom) } ?: emptyList()

val result = (nip96servers + blossomServers).ifEmpty { DEFAULT_MEDIA_SERVERS }

return result + ServerName("NIP95", "", ServerType.NIP95)
}

fun sendFileServersList(servers: List<String>) {
if (!isWriteable()) return

Expand All @@ -3688,6 +3752,31 @@ class Account(
}
}

fun sendBlossomServersList(servers: List<String>) {
if (!isWriteable()) return

val serverList = getBlossomServersList()

if (serverList != null && serverList.tags.isNotEmpty()) {
BlossomServersEvent.updateRelayList(
earlierVersion = serverList,
relays = servers,
signer = signer,
) {
Client.send(it)
LocalCache.justConsume(it, null)
}
} else {
BlossomServersEvent.createFromScratch(
relays = servers,
signer = signer,
) {
Client.send(it)
LocalCache.justConsume(it, null)
}
}
}

fun getAllPeopleLists(): List<AddressableNote> = getAllPeopleLists(signer.pubKey)

fun getAllPeopleLists(pubkey: HexKey): List<AddressableNote> =
Expand Down
Loading

0 comments on commit 2c9e2de

Please sign in to comment.