Refactors Ammolite to remove the dependency on OkHttp to prepare for KTor and multiplatform settings.

- This also reduces the Singleton coupling between Client and RelayPool.

To migrate, create a NostrClient instance on your Application class and update your code to access that `client` instance.
This commit is contained in:
Vitor Pamplona
2024-12-20 15:36:50 -05:00
parent d9c14a78a7
commit f839565152
40 changed files with 560 additions and 323 deletions

View File

@ -37,8 +37,10 @@ import coil3.memory.MemoryCache
import coil3.request.crossfade import coil3.request.crossfade
import com.vitorpamplona.amethyst.service.LocationState import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket
import com.vitorpamplona.amethyst.service.playback.VideoCache import com.vitorpamplona.amethyst.service.playback.VideoCache
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.ammolite.relays.NostrClient
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -54,8 +56,10 @@ import kotlin.time.measureTimedValue
class Amethyst : Application() { class Amethyst : Application() {
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val client: NostrClient = NostrClient(OkHttpWebSocket.Builder())
// Service Manager is only active when the activity is active. // Service Manager is only active when the activity is active.
val serviceManager = ServiceManager(applicationIOScope) val serviceManager = ServiceManager(client, applicationIOScope)
val locationManager = LocationState(this, applicationIOScope) val locationManager = LocationState(this, applicationIOScope)
val pokeyReceiver = PokeyReceiver() val pokeyReceiver = PokeyReceiver()

View File

@ -43,11 +43,9 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.RelayPool
fun debugState(context: Context) { fun debugState(context: Context) {
Client Amethyst.instance.client
.allSubscriptions() .allSubscriptions()
.forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toDebugJson() }}") } .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toDebugJson() }}") }
@ -89,7 +87,7 @@ fun debugState(context: Context) {
Log.d("STATE DUMP", "Memory Class " + memClass + " MB (largeHeap $isLargeHeap)") Log.d("STATE DUMP", "Memory Class " + memClass + " MB (largeHeap $isLargeHeap)")
} }
Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays()) Log.d("STATE DUMP", "Connected Relays: " + Amethyst.instance.client.connectedRelays())
Log.d( Log.d(
"STATE DUMP", "STATE DUMP",

View File

@ -50,12 +50,12 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.ammolite.relays.Client import com.vitorpamplona.ammolite.relays.NostrClient
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.bechToBytes import com.vitorpamplona.quartz.encoders.bechToBytes
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
@ -73,10 +73,12 @@ import kotlinx.coroutines.runBlocking
@Stable @Stable
class ServiceManager( class ServiceManager(
val client: NostrClient,
val scope: CoroutineScope, val scope: CoroutineScope,
) { ) {
private var isStarted: Boolean = // to not open amber in a loop trying to use auth relays and registering for notifications
false // to not open amber in a loop trying to use auth relays and registering for notifications private var isStarted: Boolean = false
private var account: Account? = null private var account: Account? = null
private var collectorJob: Job? = null private var collectorJob: Job? = null
@ -156,7 +158,7 @@ class ServiceManager(
if (myAccount != null) { if (myAccount != null) {
val relaySet = myAccount.connectToRelaysWithProxy.value val relaySet = myAccount.connectToRelaysWithProxy.value
Client.reconnect(relaySet) client.reconnect(relaySet)
collectorJob?.cancel() collectorJob?.cancel()
collectorJob = null collectorJob = null
@ -165,7 +167,7 @@ class ServiceManager(
myAccount.connectToRelaysWithProxy.collectLatest { myAccount.connectToRelaysWithProxy.collectLatest {
delay(500) delay(500)
if (isStarted) { if (isStarted) {
Client.reconnect(it, onlyIfChanged = true) client.reconnect(it, onlyIfChanged = true)
} }
} }
} }
@ -231,7 +233,7 @@ class ServiceManager(
NostrUserProfileDataSource.stopSync() NostrUserProfileDataSource.stopSync()
NostrVideoDataSource.stopSync() NostrVideoDataSource.stopSync()
Client.reconnect(null) client.reconnect(null)
isStarted = false isStarted = false
} }

View File

@ -34,13 +34,13 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.service.LocationState import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.tryAndWait
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS 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.ServerName
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.Constants import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.ammolite.relays.FeedType import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.ammolite.relays.Relay
@ -48,7 +48,6 @@ import com.vitorpamplona.ammolite.relays.RelaySetupInfo
import com.vitorpamplona.ammolite.relays.RelaySetupInfoToConnect import com.vitorpamplona.ammolite.relays.RelaySetupInfoToConnect
import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Dimension import com.vitorpamplona.quartz.encoders.Dimension
@ -1129,7 +1128,7 @@ class Account(
otherTags = emptyArray(), otherTags = emptyArray(),
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1146,7 +1145,7 @@ class Account(
relayUse = relays, relayUse = relays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -1160,7 +1159,7 @@ class Account(
signer = signer, signer = signer,
) { ) {
// Keep this local to avoid erasing a good contact list. // Keep this local to avoid erasing a good contact list.
// Client.send(it) // Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1202,7 +1201,7 @@ class Account(
github = github, github = github,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
@ -1278,7 +1277,7 @@ class Account(
if (emojiUrl != null) { if (emojiUrl != null) {
note.event?.let { note.event?.let {
ReactionEvent.create(emojiUrl, it, signer) { ReactionEvent.create(emojiUrl, it, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it) LocalCache.consume(it)
} }
} }
@ -1289,7 +1288,7 @@ class Account(
note.event?.let { note.event?.let {
ReactionEvent.create(reaction, it, signer) { ReactionEvent.create(reaction, it, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it) LocalCache.consume(it)
} }
} }
@ -1395,7 +1394,7 @@ class Account(
LocalCache.consume(event, zappedNote) { it.response(signer) { onResponse(it) } } LocalCache.consume(event, zappedNote) { it.response(signer) { onResponse(it) } }
Client.sendSingle( Amethyst.instance.client.sendSingle(
signedEvent = event, signedEvent = event,
relayTemplate = relayTemplate =
RelaySetupInfoToConnect( RelaySetupInfoToConnect(
@ -1448,14 +1447,14 @@ class Account(
note.event?.let { note.event?.let {
ReactionEvent.createWarning(it, signer) { ReactionEvent.createWarning(it, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
note.event?.let { note.event?.let {
ReportEvent.create(it, type, signer, content) { ReportEvent.create(it, type, signer, content) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1473,7 +1472,7 @@ class Account(
} }
ReportEvent.create(user.pubkeyHex, type, signer) { ReportEvent.create(user.pubkeyHex, type, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1490,7 +1489,7 @@ class Account(
// chunks in 200 elements to avoid going over the 65KB limit for events. // chunks in 200 elements to avoid going over the 65KB limit for events.
myNoteVersions.chunked(200).forEach { chunkedList -> myNoteVersions.chunked(200).forEach { chunkedList ->
DeletionEvent.create(chunkedList, signer) { deletionEvent -> DeletionEvent.create(chunkedList, signer) { deletionEvent ->
Client.send(deletionEvent) Amethyst.instance.client.send(deletionEvent)
LocalCache.justConsume(deletionEvent, null) LocalCache.justConsume(deletionEvent, null)
} }
} }
@ -1549,12 +1548,12 @@ class Account(
note.event?.let { note.event?.let {
if (it.kind() == 1) { if (it.kind() == 1) {
RepostEvent.create(it, signer) { RepostEvent.create(it, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
GenericRepostEvent.create(it, signer) { GenericRepostEvent.create(it, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1565,7 +1564,7 @@ class Account(
note.event?.let { note.event?.let {
if (it is WrappedEvent && it.host != null) { if (it is WrappedEvent && it.host != null) {
it.host?.let { it.host?.let {
Client.sendFilterAndStopOnFirstResponse( Amethyst.instance.client.sendFilterAndStopOnFirstResponse(
filters = filters =
listOf( listOf(
TypedFilter( TypedFilter(
@ -1576,12 +1575,12 @@ class Account(
), ),
), ),
onResponse = { onResponse = {
Client.send(it) Amethyst.instance.client.send(it)
}, },
) )
} }
} else { } else {
Client.send(it) Amethyst.instance.client.send(it)
} }
} }
} }
@ -1595,7 +1594,7 @@ class Account(
if (pair.value != newAttestation) { if (pair.value != newAttestation) {
OtsEvent.create(pair.key, newAttestation, signer) { OtsEvent.create(pair.key, newAttestation, signer) {
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
Client.send(it) Amethyst.instance.client.send(it)
settings.pendingAttestations.update { settings.pendingAttestations.update {
it - pair.key it - pair.key
@ -1626,7 +1625,7 @@ class Account(
if (contactList != null) { if (contactList != null) {
ContactListEvent.followUser(contactList, user.pubkeyHex, signer) { ContactListEvent.followUser(contactList, user.pubkeyHex, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -1642,7 +1641,7 @@ class Account(
}, },
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1655,7 +1654,7 @@ class Account(
if (contactList != null) { if (contactList != null) {
ContactListEvent.followEvent(contactList, channel.idHex, signer) { ContactListEvent.followEvent(contactList, channel.idHex, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -1671,7 +1670,7 @@ class Account(
}, },
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1684,7 +1683,7 @@ class Account(
if (contactList != null) { if (contactList != null) {
ContactListEvent.followAddressableEvent(contactList, community.address, signer) { ContactListEvent.followAddressableEvent(contactList, community.address, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -1701,7 +1700,7 @@ class Account(
relayUse = relays, relayUse = relays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1718,7 +1717,7 @@ class Account(
tag, tag,
signer, signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -1734,7 +1733,7 @@ class Account(
}, },
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -1770,7 +1769,7 @@ class Account(
} }
fun onNewEventCreated(event: Event) { fun onNewEventCreated(event: Event) {
Client.send(event) Amethyst.instance.client.send(event)
LocalCache.justConsume(event, null) LocalCache.justConsume(event, null)
} }
@ -1888,10 +1887,10 @@ class Account(
): Note? { ): Note? {
if (!isWriteable()) return null if (!isWriteable()) return null
Client.send(data, relayList = relayList) Amethyst.instance.client.send(data, relayList = relayList)
LocalCache.consume(data, null) LocalCache.consume(data, null)
Client.send(signedEvent, relayList = relayList) Amethyst.instance.client.send(signedEvent, relayList = relayList)
LocalCache.consume(signedEvent, null) LocalCache.consume(signedEvent, null)
return LocalCache.getNoteIfExists(signedEvent.id) return LocalCache.getNoteIfExists(signedEvent.id)
@ -1912,8 +1911,8 @@ class Account(
signedEvent: FileStorageHeaderEvent, signedEvent: FileStorageHeaderEvent,
relayList: List<RelaySetupInfo>, relayList: List<RelaySetupInfo>,
) { ) {
Client.send(data, relayList = relayList) Amethyst.instance.client.send(data, relayList = relayList)
Client.send(signedEvent, relayList = relayList) Amethyst.instance.client.send(signedEvent, relayList = relayList)
} }
fun sendHeader( fun sendHeader(
@ -1921,7 +1920,7 @@ class Account(
relayList: List<RelaySetupInfo>, relayList: List<RelaySetupInfo>,
onReady: (Note) -> Unit, onReady: (Note) -> Unit,
) { ) {
Client.send(signedEvent, relayList = relayList) Amethyst.instance.client.send(signedEvent, relayList = relayList)
LocalCache.justConsume(signedEvent, null) LocalCache.justConsume(signedEvent, null)
LocalCache.getNoteIfExists(signedEvent.id)?.let { onReady(it) } LocalCache.getNoteIfExists(signedEvent.id)?.let { onReady(it) }
@ -2121,13 +2120,13 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } }
addresses?.forEach { addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
} }
@ -2184,19 +2183,19 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
// broadcast replied notes // broadcast replied notes
replyingTo?.let { replyingTo?.let {
LocalCache.getNoteIfExists(replyingTo)?.event?.let { LocalCache.getNoteIfExists(replyingTo)?.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } }
addresses?.forEach { addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
} }
@ -2250,19 +2249,19 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
// broadcast replied notes // broadcast replied notes
replyingTo?.let { replyingTo?.let {
LocalCache.getNoteIfExists(replyingTo)?.event?.let { LocalCache.getNoteIfExists(replyingTo)?.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } }
addresses?.forEach { addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
} }
@ -2275,7 +2274,7 @@ class Account(
val noteEvent = note.event val noteEvent = note.event
if (noteEvent is DraftEvent) { if (noteEvent is DraftEvent) {
noteEvent.createDeletedEvent(signer) { noteEvent.createDeletedEvent(signer) {
Client.sendPrivately( Amethyst.instance.client.sendPrivately(
it, it,
note.relays.map { it.url }.map { note.relays.map { it.url }.map {
RelaySetupInfoToConnect( RelaySetupInfoToConnect(
@ -2359,11 +2358,11 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
replyingTo.event?.let { replyingTo.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
} }
@ -2391,11 +2390,11 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
replyingTo.event?.let { replyingTo.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
} }
@ -2466,11 +2465,11 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
replyingTo.event?.let { replyingTo.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
} }
@ -2497,7 +2496,7 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -2522,9 +2521,9 @@ class Account(
signer = signer, signer = signer,
) { ) {
if (relayList.isNotEmpty()) { if (relayList.isNotEmpty()) {
Client.sendPrivately(it, relayList = relayList) Amethyst.instance.client.sendPrivately(it, relayList = relayList)
} else { } else {
Client.send(it) Amethyst.instance.client.send(it)
} }
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
@ -2546,9 +2545,9 @@ class Account(
signer = signer, signer = signer,
) { ) {
if (relayList.isNotEmpty()) { if (relayList.isNotEmpty()) {
Client.sendPrivately(it, relayList = relayList) Amethyst.instance.client.sendPrivately(it, relayList = relayList)
} else { } else {
Client.send(it) Amethyst.instance.client.send(it)
} }
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
@ -2593,7 +2592,7 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -2634,7 +2633,7 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -2690,19 +2689,19 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
// broadcast replied notes // broadcast replied notes
replyingTo?.let { replyingTo?.let {
LocalCache.getNoteIfExists(replyingTo)?.event?.let { LocalCache.getNoteIfExists(replyingTo)?.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } }
addresses?.forEach { addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
} }
@ -2728,7 +2727,7 @@ class Account(
signer = signer, signer = signer,
) { ) {
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
@ -2782,14 +2781,14 @@ class Account(
} }
} }
} else { } else {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
// Rebroadcast replies and tags to the current relay set // Rebroadcast replies and tags to the current relay set
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } replyTo?.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } }
addresses?.forEach { addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList) Amethyst.instance.client.send(it, relayList = relayList)
} }
} }
} }
@ -2835,7 +2834,7 @@ class Account(
} }
} }
} else { } else {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -2881,7 +2880,7 @@ class Account(
} }
} }
} else { } else {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -2954,7 +2953,7 @@ class Account(
} }
} }
} else { } else {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} }
@ -3020,9 +3019,9 @@ class Account(
fun sendDraftEvent(draftEvent: DraftEvent) { fun sendDraftEvent(draftEvent: DraftEvent) {
val relayList = getPrivateOutBoxRelayList() val relayList = getPrivateOutBoxRelayList()
if (relayList.isNotEmpty()) { if (relayList.isNotEmpty()) {
Client.sendPrivately(draftEvent, relayList) Amethyst.instance.client.sendPrivately(draftEvent, relayList)
} else { } else {
Client.send(draftEvent) Amethyst.instance.client.send(draftEvent)
} }
LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(draftEvent, null)
} }
@ -3072,12 +3071,12 @@ class Account(
} }
if (relayList != null) { if (relayList != null) {
Client.sendPrivately(signedEvent = wrap, relayList = relayList) Amethyst.instance.client.sendPrivately(signedEvent = wrap, relayList = relayList)
} else { } else {
Client.send(wrap) Amethyst.instance.client.send(wrap)
} }
} else { } else {
Client.send(wrap) Amethyst.instance.client.send(wrap)
} }
} }
} }
@ -3095,7 +3094,7 @@ class Account(
picture = picture, picture = picture,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
LocalCache.getChannelIfExists(it.id)?.let { follow(it) } LocalCache.getChannelIfExists(it.id)?.let { follow(it) }
@ -3110,7 +3109,7 @@ class Account(
val oldEvent = oldStatus.event as? StatusEvent ?: return val oldEvent = oldStatus.event as? StatusEvent ?: return
StatusEvent.update(oldEvent, newStatus, signer) { StatusEvent.update(oldEvent, newStatus, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3119,7 +3118,7 @@ class Account(
if (!isWriteable()) return if (!isWriteable()) return
StatusEvent.create(newStatus, "general", expiration = null, signer) { StatusEvent.create(newStatus, "general", expiration = null, signer) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3129,11 +3128,11 @@ class Account(
val oldEvent = oldStatus.event as? StatusEvent ?: return val oldEvent = oldStatus.event as? StatusEvent ?: return
StatusEvent.clear(oldEvent, signer) { event -> StatusEvent.clear(oldEvent, signer) { event ->
Client.send(event) Amethyst.instance.client.send(event)
LocalCache.justConsume(event, null) LocalCache.justConsume(event, null)
DeletionEvent.createForVersionOnly(listOf(event), signer) { event2 -> DeletionEvent.createForVersionOnly(listOf(event), signer) { event2 ->
Client.send(event2) Amethyst.instance.client.send(event2)
LocalCache.justConsume(event2, null) LocalCache.justConsume(event2, null)
} }
} }
@ -3154,7 +3153,7 @@ class Account(
noteEvent.taggedAddresses().filter { it != emojiListEvent.address() }, noteEvent.taggedAddresses().filter { it != emojiListEvent.address() },
signer, signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3172,7 +3171,7 @@ class Account(
listOf(emojiListEvent.address()), listOf(emojiListEvent.address()),
signer, signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -3187,7 +3186,7 @@ class Account(
noteEvent.taggedAddresses().plus(emojiListEvent.address()), noteEvent.taggedAddresses().plus(emojiListEvent.address()),
signer, signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3218,7 +3217,7 @@ class Account(
originalHash = originalHash, */ originalHash = originalHash, */
signer = signer, signer = signer,
) { event -> ) { event ->
Client.send(event) Amethyst.instance.client.send(event)
LocalCache.consume(event, null) LocalCache.consume(event, null)
} }
} }
@ -3241,7 +3240,7 @@ class Account(
isPrivate, isPrivate,
signer, signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it) LocalCache.consume(it)
} }
} else { } else {
@ -3251,7 +3250,7 @@ class Account(
isPrivate, isPrivate,
signer, signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it) LocalCache.consume(it)
} }
} }
@ -3272,7 +3271,7 @@ class Account(
isPrivate, isPrivate,
signer, signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it) LocalCache.consume(it)
} }
} else { } else {
@ -3282,17 +3281,20 @@ class Account(
isPrivate, isPrivate,
signer, signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it) LocalCache.consume(it)
} }
} }
} }
fun createAuthEvent( fun sendAuthEvent(
relay: Relay, relay: Relay,
challenge: String, challenge: String,
onReady: (RelayAuthEvent) -> Unit, ) {
) = createAuthEvent(relay.url, challenge, onReady = onReady) createAuthEvent(relay.url, challenge) {
Amethyst.instance.client.sendIfExists(it, relay)
}
}
fun createAuthEvent( fun createAuthEvent(
relayUrl: String, relayUrl: String,
@ -3393,7 +3395,7 @@ class Account(
isPrivate = true, isPrivate = true,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} else { } else {
@ -3402,7 +3404,7 @@ class Account(
isPrivate = true, isPrivate = true,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} }
@ -3418,7 +3420,7 @@ class Account(
isPrivate = true, isPrivate = true,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} }
@ -3432,7 +3434,7 @@ class Account(
isPrivate = true, isPrivate = true,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} }
@ -3448,7 +3450,7 @@ class Account(
isPrivate = true, isPrivate = true,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} else { } else {
@ -3457,7 +3459,7 @@ class Account(
isPrivate = true, isPrivate = true,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} }
@ -3473,7 +3475,7 @@ class Account(
isPrivate = true, isPrivate = true,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} }
@ -3487,7 +3489,7 @@ class Account(
isPrivate = true, isPrivate = true,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.consume(it, null) LocalCache.consume(it, null)
} }
} }
@ -3517,7 +3519,7 @@ class Account(
originalChannelIdHex = channel.idHex, originalChannelIdHex = channel.idHex,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
follow(channel) follow(channel)
@ -3547,9 +3549,9 @@ class Account(
} }
if (relayList != null) { if (relayList != null) {
Client.sendPrivately(it, relayList) Amethyst.instance.client.sendPrivately(it, relayList)
} else { } else {
Client.send(it) Amethyst.instance.client.send(it)
} }
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
onReady(it) onReady(it)
@ -3764,7 +3766,7 @@ class Account(
relays = dmRelays, relays = dmRelays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -3772,7 +3774,7 @@ class Account(
relays = dmRelays, relays = dmRelays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3798,7 +3800,7 @@ class Account(
relays = relays, relays = relays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -3806,7 +3808,7 @@ class Account(
relays = relays, relays = relays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3832,7 +3834,7 @@ class Account(
relays = searchRelays, relays = searchRelays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -3840,7 +3842,7 @@ class Account(
relays = searchRelays, relays = searchRelays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3866,7 +3868,7 @@ class Account(
relays = relays, relays = relays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -3874,7 +3876,7 @@ class Account(
relays = relays, relays = relays,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3922,7 +3924,7 @@ class Account(
relays = servers, relays = servers,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -3930,7 +3932,7 @@ class Account(
relays = servers, relays = servers,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }
@ -3947,7 +3949,7 @@ class Account(
relays = servers, relays = servers,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} else { } else {
@ -3955,7 +3957,7 @@ class Account(
relays = servers, relays = servers,
signer = signer, signer = signer,
) { ) {
Client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
} }

View File

@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.ammolite.relays.NostrDataSource import com.vitorpamplona.ammolite.relays.NostrDataSource
import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.ammolite.relays.Relay
@ -28,7 +29,7 @@ import com.vitorpamplona.quartz.events.Event
abstract class AmethystNostrDataSource( abstract class AmethystNostrDataSource(
debugName: String, debugName: String,
) : NostrDataSource(debugName) { ) : NostrDataSource(Amethyst.instance.client, debugName) {
override fun consume( override fun consume(
event: Event, event: Event,
relay: Relay, relay: Relay,

View File

@ -27,9 +27,9 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList

View File

@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

View File

@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service
import android.util.Log import android.util.Log
import android.util.LruCache import android.util.LruCache
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.encoders.Nip11RelayInformation import com.vitorpamplona.quartz.encoders.Nip11RelayInformation
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils

View File

@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.EOSEAccount
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES
import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
@ -492,9 +491,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
super.auth(relay, challenge) super.auth(relay, challenge)
if (this::account.isInitialized) { if (this::account.isInitialized) {
account.createAuthEvent(relay, challenge) { account.sendAuthEvent(relay, challenge)
Client.sendIfExists(it, relay)
}
} }
} }

View File

@ -24,7 +24,7 @@ import android.util.Log
import android.util.LruCache import android.util.LruCache
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import okhttp3.EventListener import okhttp3.EventListener
import okhttp3.Protocol import okhttp3.Protocol

View File

@ -25,8 +25,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06 import com.vitorpamplona.quartz.encoders.Lud06
import okhttp3.Request import okhttp3.Request

View File

@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.launchAndWaitAll import com.vitorpamplona.amethyst.launchAndWaitAll
import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.tryAndWait
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.events.RelayAuthEvent import com.vitorpamplona.quartz.events.RelayAuthEvent
import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerExternal
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException

View File

@ -18,41 +18,20 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.ammolite.service package com.vitorpamplona.amethyst.service.okhttp
import android.util.Log import android.util.Log
import com.vitorpamplona.ammolite.service.HttpClientManager.setDefaultProxy import com.vitorpamplona.quartz.crypto.nip17.NostrCipher
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.IOException import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.time.Duration import java.time.Duration
class LoggingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val t1 = System.nanoTime()
val port =
(
chain
.connection()
?.route()
?.proxy
?.address() as? InetSocketAddress
)?.port
val response: Response = chain.proceed(request)
val t2 = System.nanoTime()
Log.d("OkHttpLog", "Req $port ${request.url} in ${(t2 - t1) / 1e6}ms")
return response
}
}
object HttpClientManager { object HttpClientManager {
val rootClient = val rootClient =
OkHttpClient OkHttpClient
@ -74,32 +53,32 @@ object HttpClientManager {
fun setDefaultProxy(proxy: Proxy?) { fun setDefaultProxy(proxy: Proxy?) {
if (currentProxy != proxy) { if (currentProxy != proxy) {
Log.d("HttpClient", "Changing proxy to: ${proxy != null}") Log.d("HttpClient", "Changing proxy to: ${proxy != null}")
this.currentProxy = proxy currentProxy = proxy
// recreates singleton // recreates singleton
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout) defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
} }
} }
fun getCurrentProxy(): Proxy? = this.currentProxy fun getCurrentProxy(): Proxy? = currentProxy
fun setDefaultTimeout(timeout: Duration) { fun setDefaultTimeout(timeout: Duration) {
Log.d("HttpClient", "Changing timeout to: $timeout") Log.d("HttpClient", "Changing timeout to: $timeout")
if (this.defaultTimeout.seconds != timeout.seconds) { if (defaultTimeout.seconds != timeout.seconds) {
this.defaultTimeout = timeout defaultTimeout = timeout
// recreates singleton // recreates singleton
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout) defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout) defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
} }
} }
fun setDefaultUserAgent(userAgentHeader: String) { fun setDefaultUserAgent(userAgentHeader: String) {
Log.d("HttpClient", "Changing userAgent") Log.d("HttpClient", "Changing userAgent")
if (userAgent != userAgentHeader) { if (userAgent != userAgentHeader) {
this.userAgent = userAgentHeader userAgent = userAgentHeader
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout) defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout) defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
} }
} }
@ -117,6 +96,7 @@ object HttpClientManager {
.writeTimeout(duration) .writeTimeout(duration)
.addInterceptor(DefaultContentTypeInterceptor(userAgent)) .addInterceptor(DefaultContentTypeInterceptor(userAgent))
.addNetworkInterceptor(LoggingInterceptor()) .addNetworkInterceptor(LoggingInterceptor())
.addNetworkInterceptor(EncryptedBlobInterceptor())
.build() .build()
} }
@ -135,6 +115,48 @@ object HttpClientManager {
} }
} }
class EncryptedBlobInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.isSuccessful) {
val cipher = chain.request().tag(NostrCipher::class)
println("AABBCC Cipher ${chain.request().tag(NostrCipher::class)}")
if (cipher != null) {
val body = response.peekBody(Long.MAX_VALUE)
val decryptedBytes = cipher.decrypt(body.bytes())
val newBody = decryptedBytes.toResponseBody(body.contentType())
return response.newBuilder().body(newBody).build()
}
}
return response
}
}
class LoggingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val t1 = System.nanoTime()
val port =
(
chain
.connection()
?.route()
?.proxy
?.address() as? InetSocketAddress
)?.port
val response: Response = chain.proceed(request)
val t2 = System.nanoTime()
Log.d("OkHttpLog", "Req $port ${request.url} in ${(t2 - t1) / 1e6}ms")
return response
}
}
fun getCurrentProxyPort(useProxy: Boolean): Int? = fun getCurrentProxyPort(useProxy: Boolean): Int? =
if (useProxy) { if (useProxy) {
(currentProxy?.address() as? InetSocketAddress)?.port (currentProxy?.address() as? InetSocketAddress)?.port
@ -144,13 +166,13 @@ object HttpClientManager {
fun getHttpClient(useProxy: Boolean): OkHttpClient = fun getHttpClient(useProxy: Boolean): OkHttpClient =
if (useProxy) { if (useProxy) {
if (this.defaultHttpClient == null) { if (defaultHttpClient == null) {
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout) defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
} }
defaultHttpClient!! defaultHttpClient!!
} else { } else {
if (this.defaultHttpClientWithoutProxy == null) { if (defaultHttpClientWithoutProxy == null) {
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout) defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
} }
defaultHttpClientWithoutProxy!! defaultHttpClientWithoutProxy!!
} }

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.okhttp
import com.vitorpamplona.ammolite.sockets.WebSocket
import com.vitorpamplona.ammolite.sockets.WebSocketListener
import com.vitorpamplona.ammolite.sockets.WebsocketBuilder
import okhttp3.Request
import okhttp3.Response
class OkHttpWebSocket(
val url: String,
val forceProxy: Boolean,
val out: WebSocketListener,
) : WebSocket {
private val listener = OkHttpWebsocketListener()
private var socket: okhttp3.WebSocket? = null
fun buildRequest() = Request.Builder().url(url.trim()).build()
override fun connect() {
socket = HttpClientManager.getHttpClient(forceProxy).newWebSocket(buildRequest(), listener)
}
inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() {
override fun onOpen(
webSocket: okhttp3.WebSocket,
response: Response,
) = out.onOpen(
response.receivedResponseAtMillis - response.sentRequestAtMillis,
response.headers.get("Sec-WebSocket-Extensions")?.contains("permessage-deflate") ?: false,
)
override fun onMessage(
webSocket: okhttp3.WebSocket,
text: String,
) = out.onMessage(text)
override fun onClosing(
webSocket: okhttp3.WebSocket,
code: Int,
reason: String,
) = out.onClosing(code, reason)
override fun onClosed(
webSocket: okhttp3.WebSocket,
code: Int,
reason: String,
) = out.onClosed(code, reason)
override fun onFailure(
webSocket: okhttp3.WebSocket,
t: Throwable,
response: Response?,
) = out.onFailure(t, response?.message)
}
class Builder : WebsocketBuilder {
override fun build(
url: String,
forceProxy: Boolean,
out: WebSocketListener,
) = OkHttpWebSocket(url, forceProxy, out)
}
override fun cancel() {
socket?.cancel()
}
override fun send(msg: String): Boolean = socket?.send(msg) ?: false
}

View File

@ -24,7 +24,7 @@ import android.util.Log
import android.util.LruCache import android.util.LruCache
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.ots.BitcoinExplorer import com.vitorpamplona.quartz.ots.BitcoinExplorer
import com.vitorpamplona.quartz.ots.BlockHeader import com.vitorpamplona.quartz.ots.BlockHeader
import com.vitorpamplona.quartz.ots.exceptions.UrlException import com.vitorpamplona.quartz.ots.exceptions.UrlException

View File

@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.ots package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.ots.ICalendar import com.vitorpamplona.quartz.ots.ICalendar
import com.vitorpamplona.quartz.ots.StreamDeserializationContext import com.vitorpamplona.quartz.ots.StreamDeserializationContext

View File

@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.ots package com.vitorpamplona.amethyst.service.ots
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit import com.vitorpamplona.quartz.ots.ICalendarAsyncSubmit
import com.vitorpamplona.quartz.ots.StreamDeserializationContext import com.vitorpamplona.quartz.ots.StreamDeserializationContext
import com.vitorpamplona.quartz.ots.Timestamp import com.vitorpamplona.quartz.ots.Timestamp

View File

@ -26,7 +26,7 @@ import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService import androidx.media3.session.MediaSessionService
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
class PlaybackService : MediaSessionService() { class PlaybackService : MediaSessionService() {
private var videoViewedPositionCache = VideoViewedPositionCache() private var videoViewedPositionCache = VideoViewedPositionCache()

View File

@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.previews package com.vitorpamplona.amethyst.service.previews
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

View File

@ -31,10 +31,10 @@ import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.HttpStatusMessages
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey

View File

@ -32,9 +32,9 @@ import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.HttpStatusMessages
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Dimension import com.vitorpamplona.quartz.encoders.Dimension
import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View File

@ -24,7 +24,7 @@ import android.util.Log
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okhttp3.Request import okhttp3.Request
import java.net.URI import java.net.URI

View File

@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.debugState
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.ui.components.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.ui.components.DEFAULT_MUTED_SETTING
import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
@ -46,7 +47,6 @@ import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent

View File

@ -32,7 +32,7 @@ import androidx.annotation.RequiresApi
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback

View File

@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.actions.uploads package com.vitorpamplona.amethyst.ui.actions.uploads
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View File

@ -97,6 +97,7 @@ import com.linc.audiowaveform.infiniteLinearGradient
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.commons.compose.produceCachedState
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.playback.PlaybackClientController import com.vitorpamplona.amethyst.service.playback.PlaybackClientController
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
@ -117,7 +118,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.encoders.Dimension import com.vitorpamplona.quartz.encoders.Dimension
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException

View File

@ -111,7 +111,6 @@ import com.vitorpamplona.amethyst.ui.theme.drawerSpacing
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier
import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog
import com.vitorpamplona.ammolite.relays.RelayPool
import com.vitorpamplona.ammolite.relays.RelayPoolStatus import com.vitorpamplona.ammolite.relays.RelayPoolStatus
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
@ -563,7 +562,7 @@ fun ListContent(
@Composable @Composable
private fun RelayStatus(accountViewModel: AccountViewModel) { private fun RelayStatus(accountViewModel: AccountViewModel) {
val connectedRelaysText by RelayPool.statusFlow.collectAsStateWithLifecycle(RelayPoolStatus(0, 0)) val connectedRelaysText by accountViewModel.relayStatusFlow().collectAsStateWithLifecycle(RelayPoolStatus(0, 0))
RenderRelayStatus(connectedRelaysText) RenderRelayStatus(connectedRelaysText)
} }

View File

@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier
import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.ammolite.relays.Client
import com.vitorpamplona.ammolite.relays.Constants import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair
@ -297,11 +296,11 @@ class AccountStateViewModel : ViewModel() {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
delay(2000) // waits for the new user to connect to the new relays. delay(2000) // waits for the new user to connect to the new relays.
accountSettings.backupUserMetadata?.let { Client.send(it) } accountSettings.backupUserMetadata?.let { Amethyst.instance.client.send(it) }
accountSettings.backupContactList?.let { Client.send(it) } accountSettings.backupContactList?.let { Amethyst.instance.client.send(it) }
accountSettings.backupNIP65RelayList?.let { Client.send(it) } accountSettings.backupNIP65RelayList?.let { Amethyst.instance.client.send(it) }
accountSettings.backupDMRelayList?.let { Client.send(it) } accountSettings.backupDMRelayList?.let { Amethyst.instance.client.send(it) }
accountSettings.backupSearchRelayList?.let { Client.send(it) } accountSettings.backupSearchRelayList?.let { Amethyst.instance.client.send(it) }
} }
} }
} }

View File

@ -1626,6 +1626,8 @@ class AccountViewModel(
} }
} }
fun relayStatusFlow() = Amethyst.instance.client.relayStatusFlow()
val draftNoteCache = CachedDraftNotes(this) val draftNoteCache = CachedDraftNotes(this)
class CachedDraftNotes( class CachedDraftNotes(

View File

@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash
@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.events.PictureEvent import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoEvent

View File

@ -27,7 +27,7 @@ import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import org.torproject.jni.TorService import org.torproject.jni.TorService
import org.torproject.jni.TorService.LocalBinder import org.torproject.jni.TorService.LocalBinder

View File

@ -57,7 +57,6 @@ dependencies {
implementation libs.androidx.runtime.runtime implementation libs.androidx.runtime.runtime
implementation project(path: ':quartz') implementation project(path: ':quartz')
implementation libs.okhttp
testImplementation libs.junit testImplementation libs.junit

View File

@ -22,6 +22,7 @@ package com.vitorpamplona.ammolite.relays
import android.util.Log import android.util.Log
import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.ammolite.service.checkNotInMainThread
import com.vitorpamplona.ammolite.sockets.WebsocketBuilder
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@ -37,10 +38,16 @@ import java.util.concurrent.TimeUnit
* The Nostr Client manages multiple personae the user may switch between. Events are received and * The Nostr Client manages multiple personae the user may switch between. Events are received and
* published through multiple relays. Events are stored with their respective persona. * published through multiple relays. Events are stored with their respective persona.
*/ */
object Client : RelayPool.Listener { class NostrClient(
private val websocketBuilder: WebsocketBuilder,
) : RelayPool.Listener {
private val relayPool: RelayPool = RelayPool()
private val subscriptions: MutableSubscriptionManager = MutableSubscriptionManager()
private var listeners = setOf<Listener>() private var listeners = setOf<Listener>()
private var relays = emptyArray<Relay>() private var relays = emptyArray<Relay>()
private var subscriptions = mapOf<String, List<TypedFilter>>()
fun buildRelay(it: RelaySetupInfoToConnect): Relay = Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes, websocketBuilder, subscriptions)
@Synchronized @Synchronized
fun reconnect( fun reconnect(
@ -52,33 +59,33 @@ object Client : RelayPool.Listener {
if (onlyIfChanged) { if (onlyIfChanged) {
if (!isSameRelaySetConfig(relays)) { if (!isSameRelaySetConfig(relays)) {
if (Client.relays.isNotEmpty()) { if (this.relays.isNotEmpty()) {
RelayPool.disconnect() relayPool.disconnect()
RelayPool.unregister(this) relayPool.unregister(this)
RelayPool.unloadRelays() relayPool.unloadRelays()
} }
if (relays != null) { if (relays != null) {
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes) } val newRelays = relays.map(::buildRelay)
RelayPool.register(this) relayPool.register(this)
RelayPool.loadRelays(newRelays) relayPool.loadRelays(newRelays)
RelayPool.requestAndWatch() relayPool.requestAndWatch()
Client.relays = newRelays.toTypedArray() this.relays = newRelays.toTypedArray()
} }
} }
} else { } else {
if (Client.relays.isNotEmpty()) { if (this.relays.isNotEmpty()) {
RelayPool.disconnect() relayPool.disconnect()
RelayPool.unregister(this) relayPool.unregister(this)
RelayPool.unloadRelays() relayPool.unloadRelays()
} }
if (relays != null) { if (relays != null) {
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes) } val newRelays = relays.map(::buildRelay)
RelayPool.register(this) relayPool.register(this)
RelayPool.loadRelays(newRelays) relayPool.loadRelays(newRelays)
RelayPool.requestAndWatch() relayPool.requestAndWatch()
Client.relays = newRelays.toTypedArray() this.relays = newRelays.toTypedArray()
} }
} }
} }
@ -101,8 +108,8 @@ object Client : RelayPool.Listener {
) { ) {
checkNotInMainThread() checkNotInMainThread()
subscriptions = subscriptions + Pair(subscriptionId, filters) subscriptions.add(subscriptionId, filters)
RelayPool.sendFilter(subscriptionId, filters) relayPool.sendFilter(subscriptionId, filters)
} }
fun sendFilterAndStopOnFirstResponse( fun sendFilterAndStopOnFirstResponse(
@ -129,8 +136,8 @@ object Client : RelayPool.Listener {
}, },
) )
subscriptions = subscriptions + Pair(subscriptionId, filters) subscriptions.add(subscriptionId, filters)
RelayPool.sendFilter(subscriptionId, filters) relayPool.sendFilter(subscriptionId, filters)
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@ -146,7 +153,7 @@ object Client : RelayPool.Listener {
): Boolean { ): Boolean {
checkNotInMainThread() checkNotInMainThread()
val size = if (relay != null) 1 else relayList?.size ?: RelayPool.availableRelays() val size = if (relay != null) 1 else relayList?.size ?: relayPool.availableRelays()
val latch = CountDownLatch(size) val latch = CountDownLatch(size)
val relayErrors = mutableMapOf<String, String>() val relayErrors = mutableMapOf<String, String>()
var result = false var result = false
@ -227,8 +234,8 @@ object Client : RelayPool.Listener {
) { ) {
checkNotInMainThread() checkNotInMainThread()
subscriptions = subscriptions + Pair(subscriptionId, filters) subscriptions.add(subscriptionId, filters)
RelayPool.connectAndSendFiltersIfDisconnected() relayPool.connectAndSendFiltersIfDisconnected()
} }
fun sendIfExists( fun sendIfExists(
@ -237,7 +244,7 @@ object Client : RelayPool.Listener {
) { ) {
checkNotInMainThread() checkNotInMainThread()
RelayPool.getRelays(connectedRelay.url).forEach { relayPool.getRelays(connectedRelay.url).forEach {
it.send(signedEvent) it.send(signedEvent)
} }
} }
@ -249,14 +256,14 @@ object Client : RelayPool.Listener {
) { ) {
checkNotInMainThread() checkNotInMainThread()
RelayPool.getOrCreateRelay(relayTemplate, onDone) { relayPool.runCreatingIfNeeded(buildRelay(relayTemplate), onDone = onDone) {
it.send(signedEvent) it.send(signedEvent)
} }
} }
fun send(signedEvent: EventInterface) { fun send(signedEvent: EventInterface) {
checkNotInMainThread() checkNotInMainThread()
RelayPool.send(signedEvent) relayPool.send(signedEvent)
} }
fun send( fun send(
@ -265,7 +272,7 @@ object Client : RelayPool.Listener {
) { ) {
checkNotInMainThread() checkNotInMainThread()
RelayPool.sendToSelectedRelays(relayList, signedEvent) relayPool.sendToSelectedRelays(relayList, signedEvent)
} }
fun sendPrivately( fun sendPrivately(
@ -275,18 +282,18 @@ object Client : RelayPool.Listener {
checkNotInMainThread() checkNotInMainThread()
relayList.forEach { relayTemplate -> relayList.forEach { relayTemplate ->
RelayPool.getOrCreateRelay(relayTemplate, { }) { relayPool.runCreatingIfNeeded(buildRelay(relayTemplate)) {
it.sendOverride(signedEvent) it.sendOverride(signedEvent)
} }
} }
} }
fun close(subscriptionId: String) { fun close(subscriptionId: String) {
RelayPool.close(subscriptionId) relayPool.close(subscriptionId)
subscriptions = subscriptions.minus(subscriptionId) subscriptions.remove(subscriptionId)
} }
fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId) fun isActive(subscriptionId: String): Boolean = subscriptions.isActive(subscriptionId)
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onEvent( override fun onEvent(
@ -392,9 +399,13 @@ object Client : RelayPool.Listener {
listeners = listeners.minus(listener) listeners = listeners.minus(listener)
} }
fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions.allSubscriptions()
fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList() fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions.getSubscriptionFilters(subId)
fun connectedRelays() = relayPool.connectedRelays()
fun relayStatusFlow() = relayPool.statusFlow
interface Listener { interface Listener {
/** A new message was received */ /** A new message was received */

View File

@ -35,6 +35,7 @@ import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
abstract class NostrDataSource( abstract class NostrDataSource(
val client: NostrClient,
val debugName: String, val debugName: String,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@ -67,7 +68,7 @@ abstract class NostrDataSource(
): Int = 31 * str1.hashCode() + str2.hashCode() ): Int = 31 * str1.hashCode() + str2.hashCode()
private val clientListener = private val clientListener =
object : Client.Listener { object : NostrClient.Listener {
override fun onEvent( override fun onEvent(
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
@ -139,14 +140,14 @@ abstract class NostrDataSource(
init { init {
Log.d("DataSource", "${this.javaClass.simpleName} Subscribe") Log.d("DataSource", "${this.javaClass.simpleName} Subscribe")
Client.subscribe(clientListener) client.subscribe(clientListener)
} }
fun destroy() { fun destroy() {
// makes sure to run // makes sure to run
Log.d("DataSource", "${this.javaClass.simpleName} Unsubscribe") Log.d("DataSource", "${this.javaClass.simpleName} Unsubscribe")
stop() stop()
Client.unsubscribe(clientListener) client.unsubscribe(clientListener)
scope.cancel() scope.cancel()
bundler.cancel() bundler.cancel()
} }
@ -170,7 +171,7 @@ abstract class NostrDataSource(
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
subscriptions.values.forEach { subscription -> subscriptions.values.forEach { subscription ->
Client.close(subscription.id) client.close(subscription.id)
subscription.typedFilters = null subscription.typedFilters = null
} }
} }
@ -181,7 +182,7 @@ abstract class NostrDataSource(
Log.d("DataSource", "${this.javaClass.simpleName} Stop") Log.d("DataSource", "${this.javaClass.simpleName} Stop")
subscriptions.values.forEach { subscription -> subscriptions.values.forEach { subscription ->
Client.close(subscription.id) client.close(subscription.id)
subscription.typedFilters = null subscription.typedFilters = null
} }
} }
@ -193,7 +194,7 @@ abstract class NostrDataSource(
} }
fun dismissChannel(subscription: Subscription) { fun dismissChannel(subscription: Subscription) {
Client.close(subscription.id) client.close(subscription.id)
subscriptions = subscriptions.minus(subscription.id) subscriptions = subscriptions.minus(subscription.id)
} }
@ -231,29 +232,29 @@ abstract class NostrDataSource(
subscriptions.values.forEach { updatedSubscription -> subscriptions.values.forEach { updatedSubscription ->
val updatedSubscriptionNewFilters = updatedSubscription.typedFilters val updatedSubscriptionNewFilters = updatedSubscription.typedFilters
val isActive = Client.isActive(updatedSubscription.id) val isActive = client.isActive(updatedSubscription.id)
if (!isActive && updatedSubscriptionNewFilters != null) { if (!isActive && updatedSubscriptionNewFilters != null) {
// Filter was removed from the active list // Filter was removed from the active list
if (active) { if (active) {
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
} }
} else { } else {
if (currentFilters.containsKey(updatedSubscription.id)) { if (currentFilters.containsKey(updatedSubscription.id)) {
if (updatedSubscriptionNewFilters == null) { if (updatedSubscriptionNewFilters == null) {
// was active and is not active anymore, just close. // was active and is not active anymore, just close.
Client.close(updatedSubscription.id) client.close(updatedSubscription.id)
} else { } else {
// was active and is still active, check if it has changed. // was active and is still active, check if it has changed.
if (updatedSubscription.hasChangedFiltersFrom(currentFilters[updatedSubscription.id])) { if (updatedSubscription.hasChangedFiltersFrom(currentFilters[updatedSubscription.id])) {
Client.close(updatedSubscription.id) client.close(updatedSubscription.id)
if (active) { if (active) {
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
} }
} else { } else {
// hasn't changed, does nothing. // hasn't changed, does nothing.
if (active) { if (active) {
Client.sendFilterOnlyIfDisconnected( client.sendFilterOnlyIfDisconnected(
updatedSubscription.id, updatedSubscription.id,
updatedSubscriptionNewFilters, updatedSubscriptionNewFilters,
) )
@ -269,9 +270,9 @@ abstract class NostrDataSource(
if (active) { if (active) {
Log.d( Log.d(
this@NostrDataSource.javaClass.simpleName, this@NostrDataSource.javaClass.simpleName,
"Update Filter 3 ${updatedSubscription.id} ${Client.isSubscribed(clientListener)}", "Update Filter 3 ${updatedSubscription.id} ${client.isSubscribed(clientListener)}",
) )
Client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters)
} }
} }
} }

View File

@ -22,8 +22,10 @@ package com.vitorpamplona.ammolite.relays
import android.util.Log import android.util.Log
import com.vitorpamplona.ammolite.BuildConfig import com.vitorpamplona.ammolite.BuildConfig
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.ammolite.service.checkNotInMainThread
import com.vitorpamplona.ammolite.sockets.WebSocket
import com.vitorpamplona.ammolite.sockets.WebSocketListener
import com.vitorpamplona.ammolite.sockets.WebsocketBuilder
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
@ -31,10 +33,6 @@ import com.vitorpamplona.quartz.events.RelayAuthEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
enum class FeedType { enum class FeedType {
@ -61,6 +59,8 @@ class Relay(
val write: Boolean = true, val write: Boolean = true,
val forceProxy: Boolean = false, val forceProxy: Boolean = false,
val activeTypes: Set<FeedType>, val activeTypes: Set<FeedType>,
val socketBuilder: WebsocketBuilder,
val subs: SubscriptionManager,
) { ) {
companion object { companion object {
// waits 3 minutes to reconnect once things fail // waits 3 minutes to reconnect once things fail
@ -132,13 +132,7 @@ class Relay(
lastConnectTentative = TimeUtils.now() lastConnectTentative = TimeUtils.now()
val request = socket = socketBuilder.build(url, false, RelayListener(onConnected))
Request
.Builder()
.url(url.trim())
.build()
socket = HttpClientManager.getHttpClient(forceProxy).newWebSocket(request, RelayListener(onConnected))
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
@ -153,19 +147,15 @@ class Relay(
inner class RelayListener( inner class RelayListener(
val onConnected: (Relay) -> Unit, val onConnected: (Relay) -> Unit,
) : WebSocketListener() { ) : WebSocketListener {
override fun onOpen( override fun onOpen(
webSocket: WebSocket, pingInMs: Long,
response: Response, usingCompression: Boolean,
) { ) {
checkNotInMainThread() checkNotInMainThread()
Log.d("Relay", "Connect onOpen $url $socket") Log.d("Relay", "Connect onOpen $url $socket")
markConnectionAsReady( markConnectionAsReady(pingInMs, usingCompression)
pingInMs = response.receivedResponseAtMillis - response.sentRequestAtMillis,
usingCompression =
response.headers.get("Sec-WebSocket-Extensions")?.contains("permessage-deflate") ?: false,
)
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url") // Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
onConnected(this@Relay) onConnected(this@Relay)
@ -173,10 +163,7 @@ class Relay(
listeners.forEach { it.onRelayStateChange(this@Relay, StateType.CONNECT, null) } listeners.forEach { it.onRelayStateChange(this@Relay, StateType.CONNECT, null) }
} }
override fun onMessage( override fun onMessage(text: String) {
webSocket: WebSocket,
text: String,
) {
checkNotInMainThread() checkNotInMainThread()
RelayStats.addBytesReceived(url, text.bytesUsedInMemory()) RelayStats.addBytesReceived(url, text.bytesUsedInMemory())
@ -193,7 +180,6 @@ class Relay(
} }
override fun onClosing( override fun onClosing(
webSocket: WebSocket,
code: Int, code: Int,
reason: String, reason: String,
) { ) {
@ -211,7 +197,6 @@ class Relay(
} }
override fun onClosed( override fun onClosed(
webSocket: WebSocket,
code: Int, code: Int,
reason: String, reason: String,
) { ) {
@ -225,9 +210,8 @@ class Relay(
} }
override fun onFailure( override fun onFailure(
webSocket: WebSocket,
t: Throwable, t: Throwable,
response: Response?, responseMessage: String?,
) { ) {
checkNotInMainThread() checkNotInMainThread()
@ -235,19 +219,19 @@ class Relay(
// checks if this is an actual failure. Closing the socket generates an onFailure as well. // checks if this is an actual failure. Closing the socket generates an onFailure as well.
if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) { if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) {
RelayStats.newError(url, response?.message ?: t.message ?: "onFailure event from server: ${t.javaClass.simpleName}") RelayStats.newError(url, responseMessage ?: t.message ?: "onFailure event from server: ${t.javaClass.simpleName}")
} }
// Failures disconnect the relay. // Failures disconnect the relay.
markConnectionAsClosed() markConnectionAsClosed()
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response ${t.message} $socket") Log.w("Relay", "Relay onFailure $url, $responseMessage $responseMessage ${t.message} $socket")
t.printStackTrace() t.printStackTrace()
listeners.forEach { listeners.forEach {
it.onError( it.onError(
this@Relay, this@Relay,
"", "",
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t), Error("WebSocket Failure. Response: $responseMessage. Exception: ${t.message}", t),
) )
} }
} }
@ -461,7 +445,7 @@ class Relay(
fun renewFilters() { fun renewFilters() {
// Force update all filters after AUTH. // Force update all filters after AUTH.
Client.allSubscriptions().forEach { subs.allSubscriptions().forEach {
sendFilter(requestId = it.key, it.value) sendFilter(requestId = it.key, it.value)
} }
} }

View File

@ -24,7 +24,6 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.ammolite.service.checkNotInMainThread
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@ -37,7 +36,7 @@ import kotlinx.coroutines.launch
/** /**
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
*/ */
object RelayPool : Relay.Listener { class RelayPool : Relay.Listener {
private var relays = listOf<Relay>() private var relays = listOf<Relay>()
private var listeners = setOf<Listener>() private var listeners = setOf<Listener>()
@ -57,57 +56,34 @@ object RelayPool : Relay.Listener {
fun getAll() = relays fun getAll() = relays
fun getOrCreateRelay( fun runCreatingIfNeeded(
relayTemplate: RelaySetupInfoToConnect, relay: Relay,
timeout: Long = 60000,
onDone: (() -> Unit)? = null, onDone: (() -> Unit)? = null,
whenConnected: (Relay) -> Unit, whenConnected: (Relay) -> Unit,
) { ) {
synchronized(this) { synchronized(this) {
val matching = getRelays(relayTemplate.url) val matching = getRelays(relay.url)
if (matching.isNotEmpty()) { if (matching.isNotEmpty()) {
matching.forEach { whenConnected(it) } matching.forEach { whenConnected(it) }
} else { } else {
/** temporary connection */ addRelay(relay)
newSporadicRelay(
relayTemplate.url,
relayTemplate.read,
relayTemplate.write,
relayTemplate.forceProxy,
relayTemplate.feedTypes,
onConnected = whenConnected,
onDone = onDone,
)
}
}
}
@OptIn(DelicateCoroutinesApi::class) relay.connectAndRun {
fun newSporadicRelay( relay.renewFilters()
url: String, relay.sendOutbox()
read: Boolean,
write: Boolean,
forceProxy: Boolean,
feedTypes: Set<FeedType>?,
onConnected: (Relay) -> Unit,
onDone: (() -> Unit)?,
timeout: Long = 60000,
) {
val relay = Relay(url, read, write, forceProxy, feedTypes ?: emptySet())
addRelay(relay)
relay.connectAndRun { whenConnected(relay)
relay.renewFilters()
relay.sendOutbox()
onConnected(relay) GlobalScope.launch(Dispatchers.IO) {
delay(timeout) // waits for a reply
relay.disconnect()
removeRelay(relay)
GlobalScope.launch(Dispatchers.IO) { if (onDone != null) {
delay(timeout) // waits for a reply onDone()
relay.disconnect() }
removeRelay(relay) }
if (onDone != null) {
onDone()
} }
} }
} }

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.ammolite.relays
class MutableSubscriptionManager : SubscriptionManager {
private var subscriptions = mapOf<String, List<TypedFilter>>()
fun add(
subscriptionId: String,
filters: List<TypedFilter> = listOf(),
) {
subscriptions = subscriptions + Pair(subscriptionId, filters)
}
fun remove(subscriptionId: String) {
subscriptions = subscriptions.minus(subscriptionId)
}
override fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId)
override fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions
override fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList()
}
interface SubscriptionManager {
fun isActive(subscriptionId: String): Boolean
fun allSubscriptions(): Map<String, List<TypedFilter>>
fun getSubscriptionFilters(subId: String): List<TypedFilter>
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.ammolite.sockets
interface WebSocket {
fun cancel()
fun send(msg: String): Boolean
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.ammolite.sockets
interface WebSocketListener {
fun onOpen(
pingMillis: Long,
compression: Boolean,
)
fun onMessage(text: String)
fun onClosing(
code: Int,
reason: String,
)
fun onClosed(
code: Int,
reason: String,
)
fun onFailure(
t: Throwable,
response: String?,
)
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.ammolite.sockets
interface WebsocketBuilder {
fun build(
url: String,
forceProxy: Boolean,
out: WebSocketListener,
): WebSocket
}