From 3d0b4615506c9885cfad1eeaed34945479c0f2a2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Jun 2024 10:42:00 -0400 Subject: [PATCH] Moves Relay Stats to it's own cache. Refactors the isLocalRelay function Removes unnecessary onError methods from the Relay class Moves byte counts from Int to Long --- .../amethyst/model/AntiSpamFilter.kt | 11 +- .../amethyst/model/LocalCache.kt | 11 +- .../amethyst/model/RelaySetupInfo.kt | 6 +- .../amethyst/service/HttpClientManager.kt | 10 ++ .../service/Nip11RelayInfoRetriever.kt | 3 +- .../amethyst/service/NostrDataSource.kt | 13 -- .../amethyst/service/relays/Client.kt | 20 --- .../amethyst/service/relays/Relay.kt | 149 ++++++++---------- .../amethyst/service/relays/RelayPool.kt | 7 - .../amethyst/service/relays/RelayStats.kt | 138 ++++++++++++++++ .../ui/actions/relays/AllRelayListView.kt | 3 +- .../ui/actions/relays/BasicRelaySetupInfo.kt | 6 +- .../relays/BasicRelaySetupInfoModel.kt | 15 +- .../ui/actions/relays/ByteFormatter.kt | 18 +++ .../ui/actions/relays/Kind3RelayListView.kt | 26 +-- .../actions/relays/Kind3RelayListViewModel.kt | 29 +--- .../actions/relays/Nip65RelayListViewModel.kt | 28 +--- .../ui/actions/relays/RelayStatusRow.kt | 12 +- .../ui/actions/relays/RelayUrlEditField.kt | 4 +- 19 files changed, 287 insertions(+), 222 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayStats.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index c0ddfa122..09e945e2b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.relays.Relay +import com.vitorpamplona.amethyst.service.relays.RelayStats import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip19Bech32 @@ -69,10 +70,6 @@ class AntiSpamFilter { if ( (recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null ) { - Log.w( - "Potential SPAM Message for sharing", - "${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, null)}", - ) Log.w( "Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}", @@ -81,6 +78,10 @@ class AntiSpamFilter { // Log down offenders logOffender(hash, event) + if (relay != null) { + RelayStats.newSpam(relay.url, "Potential SPAM Message ${event.id} nostr:${Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, relay.url)}") + } + liveSpam.invalidateData() return true @@ -100,7 +101,7 @@ class AntiSpamFilter { spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id))) } else { val spammer = spamMessages.get(hashCode) - spammer.duplicatedMessages = spammer.duplicatedMessages + event.id + spammer.duplicatedMessages += event.id } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 6e09fbfe2..24f8c8ce4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -450,7 +450,6 @@ object LocalCache { if (note.event != null) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } @@ -474,6 +473,8 @@ object LocalCache { val note = getOrCreateNote(event.id) val author = getOrCreateUser(event.pubKey) + // Log.d("TN", "New Response ${event.taggedEvents().joinToString(", ") { it }}}") + if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) note.addRelay(relay) @@ -623,7 +624,6 @@ object LocalCache { if (note.event != null) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } @@ -648,7 +648,6 @@ object LocalCache { if (note.event != null) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } @@ -673,7 +672,6 @@ object LocalCache { if (note.event != null) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } @@ -711,7 +709,6 @@ object LocalCache { if (note.event?.id() == event.id()) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } @@ -746,7 +743,6 @@ object LocalCache { if (note.event?.id() == event.id()) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } @@ -827,7 +823,6 @@ object LocalCache { if (note.event != null) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } @@ -1533,7 +1528,6 @@ object LocalCache { if (note.event != null) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } @@ -1572,7 +1566,6 @@ object LocalCache { if (note.event != null) return if (antiSpam.isSpam(event, relay)) { - relay?.let { it.spamCounter++ } return } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt index d0b945791..e7310cdbf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/RelaySetupInfo.kt @@ -22,16 +22,14 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.service.relays.FeedType +import com.vitorpamplona.amethyst.service.relays.RelayStat @Immutable data class RelaySetupInfo( val url: String, val read: Boolean, val write: Boolean, - val errorCount: Int = 0, - val downloadCountInBytes: Int = 0, - val uploadCountInBytes: Int = 0, - val spamCount: Int = 0, + val relayStat: RelayStat = RelayStat(), val feedTypes: Set, val paidRelay: Boolean = false, ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt index e10303f61..0bf677167 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClientManager.kt @@ -103,6 +103,16 @@ object HttpClientManager { } } + fun getHttpClientForUrl(url: String): OkHttpClient { + // TODO: How to identify relays on the local network? + val isLocalHost = url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost") + return if (isLocalHost) { + getHttpClient(false) + } else { + getHttpClient() + } + } + fun getHttpClient(useProxy: Boolean = true): OkHttpClient { return if (useProxy) { if (this.defaultHttpClient == null) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index c43a15279..983500965 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -112,9 +112,8 @@ class Nip11Retriever { try { val request: Request = Request.Builder().header("Accept", "application/nostr+json").url(url).build() - val isLocalHost = dirtyUrl.startsWith("ws://127.0.0.1") || dirtyUrl.startsWith("ws://localhost") - HttpClientManager.getHttpClient(useProxy = !isLocalHost) + HttpClientManager.getHttpClientForUrl(dirtyUrl) .newCall(request) .enqueue( object : Callback { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 8e5314f85..501ee95af 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -92,19 +92,6 @@ abstract class NostrDataSource(val debugName: String) { } } - override fun onError( - error: Error, - subscriptionId: String, - relay: Relay, - ) { - // if (subscriptions.containsKey(subscriptionId)) { - // Log.e( - // this@NostrDataSource.javaClass.simpleName, - // "Relay OnError ${relay.url}: ${error.message}" - // ) - // } - } - override fun onRelayStateChange( type: Relay.StateType, relay: Relay, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 5c2555ce5..3b4267df0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -194,19 +194,6 @@ object Client : RelayPool.Listener { } } - @OptIn(DelicateCoroutinesApi::class) - override fun onError( - error: Error, - subscriptionId: String, - relay: Relay, - ) { - // Releases the Web thread for the new payload. - // May need to add a processing queue if processing new events become too costly. - GlobalScope.launch(Dispatchers.Default) { - listeners.forEach { it.onError(error, subscriptionId, relay) } - } - } - @OptIn(DelicateCoroutinesApi::class) override fun onRelayStateChange( type: Relay.StateType, @@ -284,13 +271,6 @@ object Client : RelayPool.Listener { afterEOSE: Boolean, ) = Unit - /** A new or repeat message was received */ - open fun onError( - error: Error, - subscriptionId: String, - relay: Relay, - ) = Unit - /** Connected to or disconnected from a relay */ open fun onRelayStateChange( type: Relay.StateType, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index d4c1ee231..dc9930431 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -59,38 +59,24 @@ class Relay( val write: Boolean = true, val activeTypes: Set = FeedType.values().toSet(), ) { - val brief = RelayBriefInfoCache.get(url) - companion object { // waits 3 minutes to reconnect once things fail const val RECONNECTING_IN_SECONDS = 60 * 3 } - private val httpClient = - if (url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost")) { - HttpClientManager.getHttpClient(false) - } else { - HttpClientManager.getHttpClient() - } + val brief = RelayBriefInfoCache.get(url) private var listeners = setOf() private var socket: WebSocket? = null private var isReady: Boolean = false private var usingCompression: Boolean = false - var eventDownloadCounterInBytes = 0 - var eventUploadCounterInBytes = 0 + private var lastConnectTentative: Long = 0L - var spamCounter = 0 - var errorCounter = 0 - var pingInMs: Long? = null + private var afterEOSEPerSubscription = mutableMapOf() - var lastConnectTentative: Long = 0L - - var afterEOSEPerSubscription = mutableMapOf() - - val authResponse = mutableMapOf() - val sendWhenReady = mutableListOf() + private val authResponse = mutableMapOf() + private val sendWhenReady = mutableListOf() fun register(listener: Listener) { listeners = listeners.plus(listener) @@ -116,7 +102,7 @@ class Relay( private var connectingBlock = AtomicBoolean() fun connectAndRun(onConnected: (Relay) -> Unit) { - Log.d("Relay", "Relay.connect $url connecting: ${connectingBlock.get()} hasProxy: ${this.httpClient.proxy != null}") + Log.d("Relay", "Relay.connect $url isAlreadyConnecting: ${connectingBlock.get()}") // BRB is crashing OkHttp Deflater object :( if (url.contains("brb.io")) return @@ -141,13 +127,13 @@ class Relay( .url(url.trim()) .build() - socket = httpClient.newWebSocket(request, RelayListener(onConnected)) + socket = HttpClientManager.getHttpClientForUrl(url).newWebSocket(request, RelayListener(onConnected)) } catch (e: Exception) { if (e is CancellationException) throw e - errorCounter++ + RelayStats.newError(url, e.message) + markConnectionAsClosed() - Log.e("Relay", "Relay Invalid $url") e.printStackTrace() } finally { connectingBlock.set(false) @@ -187,7 +173,7 @@ class Relay( ) { checkNotInMainThread() - eventDownloadCounterInBytes += text.bytesUsedInMemory() + RelayStats.addBytesReceived(url, text.bytesUsedInMemory()) try { processNewRelayMessage(text) @@ -239,20 +225,23 @@ class Relay( ) { checkNotInMainThread() - errorCounter++ - socket?.cancel() // 1000, "Normal close" // Failures disconnect the relay. markConnectionAsClosed() - Log.w("Relay", "Relay onFailure $url, ${response?.message} $response") - t.printStackTrace() - listeners.forEach { - it.onError( - this@Relay, - "", - Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t), - ) + // checks if this is an actual failure. Closing the socket generates an onFailure as well. + if (!(socket == null && t.message == "Socket closed")) { + RelayStats.newError(url, response?.message ?: t.message) + + Log.w("Relay", "Relay onFailure $url, ${response?.message} $response ${t.message} $socket") + t.printStackTrace() + listeners.forEach { + it.onError( + this@Relay, + "", + Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t), + ) + } } } } @@ -263,8 +252,9 @@ class Relay( ) { this.resetEOSEStatuses() this.isReady = true - this.pingInMs = pingInMs this.usingCompression = usingCompression + + RelayStats.setPing(url, pingInMs) } fun markConnectionAsClosed() { @@ -306,6 +296,8 @@ class Relay( val message = msgArray.get(1).asText() Log.w("Relay", "Relay onNotice $url, $message") + RelayStats.newNotice(url, message) + it.onError(this@Relay, message, Error("Relay sent notice: $message")) } "OK" -> @@ -336,7 +328,9 @@ class Relay( it.onNotify(this@Relay, msgArray[1].asText()) } "CLOSED" -> listeners.forEach { Log.w("Relay", "Relay onClosed $url, $newMessage") } - else -> + else -> { + RelayStats.newError(url, "Unsupported message: $newMessage") + listeners.forEach { Log.w("Relay", "Unsupported message: $newMessage") it.onError( @@ -345,6 +339,7 @@ class Relay( Error("Unknown type $type on channel. Msg was $newMessage"), ) } + } } } @@ -389,10 +384,8 @@ class Relay( it.filter.toJson(url) } - // Log.d("Relay", "onFilterSent $url $requestId $request") + writeToSocket(request) - socket?.send(request) - eventUploadCounterInBytes += request.bytesUsedInMemory() resetEOSEStatuses() } } @@ -457,30 +450,9 @@ class Relay( checkNotInMainThread() if (signedEvent is RelayAuthEvent) { - authResponse.put(signedEvent.id, false) - // specific protocol for this event. - val event = """["AUTH",${signedEvent.toJson()}]""" - socket?.send(event) - eventUploadCounterInBytes += event.bytesUsedInMemory() + sendAuth(signedEvent) } else { - val event = """["EVENT",${signedEvent.toJson()}]""" - if (isConnected()) { - if (isReady) { - socket?.send(event) - eventUploadCounterInBytes += event.bytesUsedInMemory() - } - } else { - // sends all filters after connection is successful. - connectAndRun { - checkNotInMainThread() - - socket?.send(event) - eventUploadCounterInBytes += event.bytesUsedInMemory() - - // Sends everything. - renewFilters() - } - } + sendEvent(signedEvent) } } @@ -488,18 +460,12 @@ class Relay( checkNotInMainThread() if (signedEvent is RelayAuthEvent) { - authResponse.put(signedEvent.id, false) - // specific protocol for this event. - val event = """["AUTH",${signedEvent.toJson()}]""" - socket?.send(event) - eventUploadCounterInBytes += event.bytesUsedInMemory() + sendAuth(signedEvent) } else { if (write) { - val event = """["EVENT",${signedEvent.toJson()}]""" if (isConnected()) { if (isReady) { - socket?.send(event) - eventUploadCounterInBytes += event.bytesUsedInMemory() + writeToSocket("""["EVENT",${signedEvent.toJson()}]""") } else { synchronized(sendWhenReady) { sendWhenReady.add(signedEvent) @@ -508,10 +474,7 @@ class Relay( } else { // sends all filters after connection is successful. connectAndRun { - checkNotInMainThread() - - socket?.send(event) - eventUploadCounterInBytes += event.bytesUsedInMemory() + writeToSocket("""["EVENT",${signedEvent.toJson()}]""") // Sends everything. renewFilters() @@ -521,12 +484,40 @@ class Relay( } } - fun close(subscriptionId: String) { - checkNotInMainThread() + private fun sendAuth(signedEvent: RelayAuthEvent) { + authResponse.put(signedEvent.id, false) + writeToSocket("""["AUTH",${signedEvent.toJson()}]""") + } - val msg = """["CLOSE","$subscriptionId"]""" - // Log.d("Relay", "Close Subscription $url $msg") - socket?.send(msg) + private fun sendEvent(signedEvent: EventInterface) { + if (isConnected()) { + if (isReady) { + writeToSocket("""["EVENT",${signedEvent.toJson()}]""") + } + } else { + // sends all filters after connection is successful. + connectAndRun { + writeToSocket("""["EVENT",${signedEvent.toJson()}]""") + + // Sends everything. + renewFilters() + } + } + } + + private fun writeToSocket(str: String) { + socket?.let { + checkNotInMainThread() + + it.send(str) + RelayStats.addBytesSent(url, str.bytesUsedInMemory()) + + Log.d("Relay", "Relay send $url $str") + } + } + + fun close(subscriptionId: String) { + writeToSocket("""["CLOSE","$subscriptionId"]""") } fun isSameRelayConfig(other: Relay): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index 969a726b7..59d4638f1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -197,12 +197,6 @@ object RelayPool : Relay.Listener { afterEOSE: Boolean, ) - fun onError( - error: Error, - subscriptionId: String, - relay: Relay, - ) - fun onRelayStateChange( type: Relay.StateType, relay: Relay, @@ -241,7 +235,6 @@ object RelayPool : Relay.Listener { subscriptionId: String, error: Error, ) { - listeners.forEach { it.onError(error, subscriptionId, relay) } updateStatus() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayStats.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayStats.kt new file mode 100644 index 000000000..60a05958d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayStats.kt @@ -0,0 +1,138 @@ +/** + * 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.relays + +import androidx.collection.LruCache +import com.vitorpamplona.quartz.utils.TimeUtils + +object RelayStats { + private val innerCache = mutableMapOf() + + fun get(url: String): RelayStat { + return innerCache.getOrPut(url) { RelayStat() } + } + + fun addBytesReceived( + url: String, + bytesUsedInMemory: Int, + ) { + get(url).addBytesReceived(bytesUsedInMemory) + } + + fun addBytesSent( + url: String, + bytesUsedInMemory: Int, + ) { + get(url).addBytesSent(bytesUsedInMemory) + } + + fun newError( + url: String, + error: String?, + ) { + get(url).newError(error) + } + + fun newNotice( + url: String, + notice: String?, + ) { + get(url).newNotice(notice) + } + + fun setPing( + url: String, + pingInMs: Long, + ) { + get(url).pingInMs = pingInMs + } + + fun newSpam( + url: String, + explanation: String, + ) { + get(url).newSpam(explanation) + } +} + +class RelayStat( + var receivedBytes: Long = 0L, + var sentBytes: Long = 0L, + var spamCounter: Long = 0L, + var errorCounter: Long = 0L, + var pingInMs: Long = 0L, +) { + val messages = LruCache(100) + + fun newNotice(notice: String?) { + val debugMessage = + RelayDebugMessage( + type = RelayDebugMessageType.NOTICE, + message = notice ?: "No error message provided", + ) + + messages.put(debugMessage, debugMessage) + } + + fun newError(error: String?) { + errorCounter++ + + val debugMessage = + RelayDebugMessage( + type = RelayDebugMessageType.ERROR, + message = error ?: "No error message provided", + ) + + messages.put(debugMessage, debugMessage) + } + + fun addBytesReceived(bytesUsedInMemory: Int) { + receivedBytes += bytesUsedInMemory + } + + fun addBytesSent(bytesUsedInMemory: Int) { + sentBytes += bytesUsedInMemory + } + + fun newSpam(spamDescriptor: String) { + spamCounter++ + + val debugMessage = + RelayDebugMessage( + type = RelayDebugMessageType.SPAM, + message = spamDescriptor, + ) + + messages.put(debugMessage, debugMessage) + } +} + +class RelayDebugMessage( + val type: RelayDebugMessageType, + val message: String, + val time: Long = TimeUtils.now(), +) + +enum class RelayDebugMessageType { + SPAM, + NOTICE, + ERROR, +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt index e9ea6bc78..f19d7896c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/AllRelayListView.kt @@ -48,6 +48,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relays.Constants +import com.vitorpamplona.amethyst.service.relays.RelayStat import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -249,7 +250,7 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) { OutlinedButton( onClick = { postViewModel.deleteAll() - Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it)) } + Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) } postViewModel.loadRelayDocuments() }, ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfo.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfo.kt index 0f8fc18bc..7ae0cbc9e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfo.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfo.kt @@ -22,14 +22,12 @@ package com.vitorpamplona.amethyst.ui.actions.relays import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.RelayBriefInfoCache +import com.vitorpamplona.amethyst.service.relays.RelayStat @Immutable data class BasicRelaySetupInfo( val url: String, - val errorCount: Int = 0, - val downloadCountInBytes: Int = 0, - val uploadCountInBytes: Int = 0, - val spamCount: Int = 0, + val relayStat: RelayStat, val paidRelay: Boolean = false, ) { val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfoModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfoModel.kt index da4339aaf..07a26e7f2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfoModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/BasicRelaySetupInfoModel.kt @@ -24,7 +24,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever -import com.vitorpamplona.amethyst.service.relays.RelayPool +import com.vitorpamplona.amethyst.service.relays.RelayStats import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -78,20 +78,11 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { val relayList = getRelayList() ?: emptyList() relayList.map { relayUrl -> - val liveRelay = RelayPool.getRelay(relayUrl) - val errorCounter = liveRelay?.errorCounter ?: 0 - val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0 - val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0 - val spamCounter = liveRelay?.spamCounter ?: 0 - BasicRelaySetupInfo( relayUrl, - errorCounter, - eventDownloadCounter, - eventUploadCounter, - spamCounter, + RelayStats.get(relayUrl), ) - }.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed() + }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/ByteFormatter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/ByteFormatter.kt index 7a5bced1f..9b3679c06 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/ByteFormatter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/ByteFormatter.kt @@ -28,6 +28,14 @@ fun countToHumanReadableBytes(counter: Int) = else -> "$counter" } +fun countToHumanReadableBytes(counter: Long) = + when { + counter >= 1000000000 -> "${Math.round(counter / 1000000000f)} GB" + counter >= 1000000 -> "${Math.round(counter / 1000000f)} MB" + counter >= 1000 -> "${Math.round(counter / 1000f)} KB" + else -> "$counter" + } + fun countToHumanReadable( counter: Int, str: String, @@ -37,3 +45,13 @@ fun countToHumanReadable( counter >= 1000 -> "${Math.round(counter / 1000f)}K $str" else -> "$counter $str" } + +fun countToHumanReadable( + counter: Long, + str: String, +) = when { + counter >= 1000000000 -> "${Math.round(counter / 1000000000f)}G $str" + counter >= 1000000 -> "${Math.round(counter / 1000000f)}M $str" + counter >= 1000 -> "${Math.round(counter / 1000f)}K $str" + else -> "$counter $str" +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt index 7d1ff6ec6..1aa78cbc7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListView.kt @@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType +import com.vitorpamplona.amethyst.service.relays.RelayStat import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -151,10 +152,13 @@ fun ServerConfigPreview() { url = "nostr.mom", read = true, write = true, - errorCount = 23, - downloadCountInBytes = 10000, - uploadCountInBytes = 10000000, - spamCount = 10, + relayStat = + RelayStat( + errorCounter = 23, + receivedBytes = 10000, + sentBytes = 10000000, + spamCounter = 10, + ), feedTypes = Constants.activeTypesGlobalChats, paidRelay = true, ), @@ -370,7 +374,7 @@ private fun StatusRow( ) Text( - text = countToHumanReadableBytes(item.downloadCountInBytes), + text = countToHumanReadableBytes(item.relayStat.receivedBytes), maxLines = 1, fontSize = 12.sp, modifier = modifier, @@ -406,7 +410,7 @@ private fun StatusRow( ) Text( - text = countToHumanReadableBytes(item.uploadCountInBytes), + text = countToHumanReadableBytes(item.relayStat.sentBytes), maxLines = 1, fontSize = 12.sp, modifier = modifier, @@ -432,7 +436,7 @@ private fun StatusRow( }, ), tint = - if (item.errorCount > 0) { + if (item.relayStat.errorCounter > 0) { MaterialTheme.colorScheme.warningColor } else { MaterialTheme.colorScheme.allGoodColor @@ -440,7 +444,7 @@ private fun StatusRow( ) Text( - text = countToHumanReadable(item.errorCount, "errors"), + text = countToHumanReadable(item.relayStat.errorCounter, "errors"), maxLines = 1, fontSize = 12.sp, modifier = modifier, @@ -466,7 +470,7 @@ private fun StatusRow( }, ), tint = - if (item.spamCount > 0) { + if (item.relayStat.spamCounter > 0) { MaterialTheme.colorScheme.warningColor } else { MaterialTheme.colorScheme.allGoodColor @@ -474,7 +478,7 @@ private fun StatusRow( ) Text( - text = countToHumanReadable(item.spamCount, "spam"), + text = countToHumanReadable(item.relayStat.spamCounter, "spam"), maxLines = 1, fontSize = 12.sp, modifier = modifier, @@ -739,7 +743,7 @@ fun Kind3RelayEditBox( addedWSS, read, write, - feedTypes = FeedType.values().toSet(), + feedTypes = FeedType.entries.toSet(), ), ) url = "" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt index b98e0b409..4d09dbb35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Kind3RelayListViewModel.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType -import com.vitorpamplona.amethyst.service.relays.RelayPool +import com.vitorpamplona.amethyst.service.relays.RelayStats import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -80,7 +80,6 @@ class Kind3RelayListViewModel : ViewModel() { if (relayFile != null) { relayFile .map { - val liveRelay = RelayPool.getRelay(it.key) val localInfoFeedTypes = account.localRelays .filter { localRelay -> localRelay.url == it.key } @@ -92,48 +91,30 @@ class Kind3RelayListViewModel : ViewModel() { ?.feedTypes ?: FeedType.values().toSet().toImmutableSet() - val errorCounter = liveRelay?.errorCounter ?: 0 - val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0 - val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0 - val spamCounter = liveRelay?.spamCounter ?: 0 - RelaySetupInfo( it.key, it.value.read, it.value.write, - errorCounter, - eventDownloadCounter, - eventUploadCounter, - spamCounter, + RelayStats.get(it.key), localInfoFeedTypes, ) } .distinctBy { it.url } - .sortedBy { it.downloadCountInBytes } + .sortedBy { it.relayStat.receivedBytes } .reversed() } else { account.localRelays .map { - val liveRelay = RelayPool.getRelay(it.url) - - val errorCounter = liveRelay?.errorCounter ?: 0 - val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0 - val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0 - val spamCounter = liveRelay?.spamCounter ?: 0 - RelaySetupInfo( it.url, it.read, it.write, - errorCounter, - eventDownloadCounter, - eventUploadCounter, - spamCounter, + RelayStats.get(it.url), it.feedTypes, ) } .distinctBy { it.url } - .sortedBy { it.downloadCountInBytes } + .sortedBy { it.relayStat.receivedBytes } .reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Nip65RelayListViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Nip65RelayListViewModel.kt index 0fcd184b8..fa3f6afdc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Nip65RelayListViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/Nip65RelayListViewModel.kt @@ -24,7 +24,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever -import com.vitorpamplona.amethyst.service.relays.RelayPool +import com.vitorpamplona.amethyst.service.relays.RelayStats import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -105,40 +105,22 @@ class Nip65RelayListViewModel : ViewModel() { val relayList = account.getNIP65RelayList()?.writeRelays() ?: emptyList() relayList.map { relayUrl -> - val liveRelay = RelayPool.getRelay(relayUrl) - val errorCounter = liveRelay?.errorCounter ?: 0 - val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0 - val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0 - val spamCounter = liveRelay?.spamCounter ?: 0 - BasicRelaySetupInfo( relayUrl, - errorCounter, - eventDownloadCounter, - eventUploadCounter, - spamCounter, + RelayStats.get(relayUrl), ) - }.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed() + }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed() } _notificationRelays.update { val relayList = account.getNIP65RelayList()?.readRelays() ?: emptyList() relayList.map { relayUrl -> - val liveRelay = RelayPool.getRelay(relayUrl) - val errorCounter = liveRelay?.errorCounter ?: 0 - val eventDownloadCounter = liveRelay?.eventDownloadCounterInBytes ?: 0 - val eventUploadCounter = liveRelay?.eventUploadCounterInBytes ?: 0 - val spamCounter = liveRelay?.spamCounter ?: 0 - BasicRelaySetupInfo( relayUrl, - errorCounter, - eventDownloadCounter, - eventUploadCounter, - spamCounter, + RelayStats.get(relayUrl), ) - }.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed() + }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayStatusRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayStatusRow.kt index 99cc7ac71..755a3566d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayStatusRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayStatusRow.kt @@ -75,7 +75,7 @@ fun RelayStatusRow( ) Text( - text = countToHumanReadableBytes(item.downloadCountInBytes), + text = countToHumanReadableBytes(item.relayStat.receivedBytes), maxLines = 1, fontSize = 12.sp, modifier = modifier, @@ -101,7 +101,7 @@ fun RelayStatusRow( ) Text( - text = countToHumanReadableBytes(item.uploadCountInBytes), + text = countToHumanReadableBytes(item.relayStat.sentBytes), maxLines = 1, fontSize = 12.sp, modifier = modifier, @@ -124,7 +124,7 @@ fun RelayStatusRow( }, ), tint = - if (item.errorCount > 0) { + if (item.relayStat.errorCounter > 0) { MaterialTheme.colorScheme.warningColor } else { MaterialTheme.colorScheme.allGoodColor @@ -132,7 +132,7 @@ fun RelayStatusRow( ) Text( - text = countToHumanReadable(item.errorCount, "errors"), + text = countToHumanReadable(item.relayStat.errorCounter, "errors"), maxLines = 1, fontSize = 12.sp, modifier = modifier, @@ -163,7 +163,7 @@ fun RelayStatusRow( }, ), tint = - if (item.spamCount > 0) { + if (item.relayStat.spamCounter > 0) { MaterialTheme.colorScheme.warningColor } else { MaterialTheme.colorScheme.allGoodColor @@ -171,7 +171,7 @@ fun RelayStatusRow( ) Text( - text = countToHumanReadable(item.spamCount, "spam"), + text = countToHumanReadable(item.relayStat.spamCounter, "spam"), maxLines = 1, fontSize = 12.sp, modifier = modifier, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayUrlEditField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayUrlEditField.kt index 1b5910f31..9af9c8326 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayUrlEditField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/relays/RelayUrlEditField.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.relays.RelayStat import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -65,8 +66,7 @@ fun RelayUrlEditField(onNewRelay: (BasicRelaySetupInfo) -> Unit) { Button( onClick = { if (url.isNotBlank() && url != "/") { - val addedWSS = RelayUrlFormatter.normalize(url) - onNewRelay(BasicRelaySetupInfo(addedWSS)) + onNewRelay(BasicRelaySetupInfo(RelayUrlFormatter.normalize(url), RelayStat())) url = "" } },