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
This commit is contained in:
Vitor Pamplona
2024-06-03 10:42:00 -04:00
parent 5900e6c49f
commit 3d0b461550
19 changed files with 287 additions and 222 deletions
@@ -26,6 +26,7 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.RelayStats
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip19Bech32
@@ -69,10 +70,6 @@ class AntiSpamFilter {
if ( if (
(recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null (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( Log.w(
"Potential SPAM Message", "Potential SPAM Message",
"${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}",
@@ -81,6 +78,10 @@ class AntiSpamFilter {
// Log down offenders // Log down offenders
logOffender(hash, event) 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() liveSpam.invalidateData()
return true return true
@@ -100,7 +101,7 @@ class AntiSpamFilter {
spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id))) spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id)))
} else { } else {
val spammer = spamMessages.get(hashCode) val spammer = spamMessages.get(hashCode)
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id spammer.duplicatedMessages += event.id
} }
} }
@@ -450,7 +450,6 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -474,6 +473,8 @@ object LocalCache {
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
// Log.d("TN", "New Response ${event.taggedEvents().joinToString(", ") { it }}}")
if (relay != null) { if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt) author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay) note.addRelay(relay)
@@ -623,7 +624,6 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -648,7 +648,6 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -673,7 +672,6 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -711,7 +709,6 @@ object LocalCache {
if (note.event?.id() == event.id()) return if (note.event?.id() == event.id()) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -746,7 +743,6 @@ object LocalCache {
if (note.event?.id() == event.id()) return if (note.event?.id() == event.id()) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -827,7 +823,6 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -1533,7 +1528,6 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -1572,7 +1566,6 @@ object LocalCache {
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event, relay)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { it.spamCounter++ }
return return
} }
@@ -22,16 +22,14 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.RelayStat
@Immutable @Immutable
data class RelaySetupInfo( data class RelaySetupInfo(
val url: String, val url: String,
val read: Boolean, val read: Boolean,
val write: Boolean, val write: Boolean,
val errorCount: Int = 0, val relayStat: RelayStat = RelayStat(),
val downloadCountInBytes: Int = 0,
val uploadCountInBytes: Int = 0,
val spamCount: Int = 0,
val feedTypes: Set<FeedType>, val feedTypes: Set<FeedType>,
val paidRelay: Boolean = false, val paidRelay: Boolean = false,
) { ) {
@@ -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 { fun getHttpClient(useProxy: Boolean = true): OkHttpClient {
return if (useProxy) { return if (useProxy) {
if (this.defaultHttpClient == null) { if (this.defaultHttpClient == null) {
@@ -112,9 +112,8 @@ class Nip11Retriever {
try { try {
val request: Request = val request: Request =
Request.Builder().header("Accept", "application/nostr+json").url(url).build() 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) .newCall(request)
.enqueue( .enqueue(
object : Callback { object : Callback {
@@ -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( override fun onRelayStateChange(
type: Relay.StateType, type: Relay.StateType,
relay: Relay, relay: Relay,
@@ -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) @OptIn(DelicateCoroutinesApi::class)
override fun onRelayStateChange( override fun onRelayStateChange(
type: Relay.StateType, type: Relay.StateType,
@@ -284,13 +271,6 @@ object Client : RelayPool.Listener {
afterEOSE: Boolean, afterEOSE: Boolean,
) = Unit ) = 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 */ /** Connected to or disconnected from a relay */
open fun onRelayStateChange( open fun onRelayStateChange(
type: Relay.StateType, type: Relay.StateType,
@@ -59,38 +59,24 @@ class Relay(
val write: Boolean = true, val write: Boolean = true,
val activeTypes: Set<FeedType> = FeedType.values().toSet(), val activeTypes: Set<FeedType> = FeedType.values().toSet(),
) { ) {
val brief = RelayBriefInfoCache.get(url)
companion object { companion object {
// waits 3 minutes to reconnect once things fail // waits 3 minutes to reconnect once things fail
const val RECONNECTING_IN_SECONDS = 60 * 3 const val RECONNECTING_IN_SECONDS = 60 * 3
} }
private val httpClient = val brief = RelayBriefInfoCache.get(url)
if (url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost")) {
HttpClientManager.getHttpClient(false)
} else {
HttpClientManager.getHttpClient()
}
private var listeners = setOf<Listener>() private var listeners = setOf<Listener>()
private var socket: WebSocket? = null private var socket: WebSocket? = null
private var isReady: Boolean = false private var isReady: Boolean = false
private var usingCompression: Boolean = false private var usingCompression: Boolean = false
var eventDownloadCounterInBytes = 0 private var lastConnectTentative: Long = 0L
var eventUploadCounterInBytes = 0
var spamCounter = 0 private var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
var errorCounter = 0
var pingInMs: Long? = null
var lastConnectTentative: Long = 0L private val authResponse = mutableMapOf<HexKey, Boolean>()
private val sendWhenReady = mutableListOf<EventInterface>()
var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
val authResponse = mutableMapOf<HexKey, Boolean>()
val sendWhenReady = mutableListOf<EventInterface>()
fun register(listener: Listener) { fun register(listener: Listener) {
listeners = listeners.plus(listener) listeners = listeners.plus(listener)
@@ -116,7 +102,7 @@ class Relay(
private var connectingBlock = AtomicBoolean() private var connectingBlock = AtomicBoolean()
fun connectAndRun(onConnected: (Relay) -> Unit) { 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 :( // BRB is crashing OkHttp Deflater object :(
if (url.contains("brb.io")) return if (url.contains("brb.io")) return
@@ -141,13 +127,13 @@ class Relay(
.url(url.trim()) .url(url.trim())
.build() .build()
socket = httpClient.newWebSocket(request, RelayListener(onConnected)) socket = HttpClientManager.getHttpClientForUrl(url).newWebSocket(request, RelayListener(onConnected))
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
errorCounter++ RelayStats.newError(url, e.message)
markConnectionAsClosed() markConnectionAsClosed()
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace() e.printStackTrace()
} finally { } finally {
connectingBlock.set(false) connectingBlock.set(false)
@@ -187,7 +173,7 @@ class Relay(
) { ) {
checkNotInMainThread() checkNotInMainThread()
eventDownloadCounterInBytes += text.bytesUsedInMemory() RelayStats.addBytesReceived(url, text.bytesUsedInMemory())
try { try {
processNewRelayMessage(text) processNewRelayMessage(text)
@@ -239,20 +225,23 @@ class Relay(
) { ) {
checkNotInMainThread() checkNotInMainThread()
errorCounter++
socket?.cancel() // 1000, "Normal close" socket?.cancel() // 1000, "Normal close"
// Failures disconnect the relay. // Failures disconnect the relay.
markConnectionAsClosed() markConnectionAsClosed()
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response") // checks if this is an actual failure. Closing the socket generates an onFailure as well.
t.printStackTrace() if (!(socket == null && t.message == "Socket closed")) {
listeners.forEach { RelayStats.newError(url, response?.message ?: t.message)
it.onError(
this@Relay, Log.w("Relay", "Relay onFailure $url, ${response?.message} $response ${t.message} $socket")
"", t.printStackTrace()
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t), listeners.forEach {
) it.onError(
this@Relay,
"",
Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t),
)
}
} }
} }
} }
@@ -263,8 +252,9 @@ class Relay(
) { ) {
this.resetEOSEStatuses() this.resetEOSEStatuses()
this.isReady = true this.isReady = true
this.pingInMs = pingInMs
this.usingCompression = usingCompression this.usingCompression = usingCompression
RelayStats.setPing(url, pingInMs)
} }
fun markConnectionAsClosed() { fun markConnectionAsClosed() {
@@ -306,6 +296,8 @@ class Relay(
val message = msgArray.get(1).asText() val message = msgArray.get(1).asText()
Log.w("Relay", "Relay onNotice $url, $message") Log.w("Relay", "Relay onNotice $url, $message")
RelayStats.newNotice(url, message)
it.onError(this@Relay, message, Error("Relay sent notice: $message")) it.onError(this@Relay, message, Error("Relay sent notice: $message"))
} }
"OK" -> "OK" ->
@@ -336,7 +328,9 @@ class Relay(
it.onNotify(this@Relay, msgArray[1].asText()) it.onNotify(this@Relay, msgArray[1].asText())
} }
"CLOSED" -> listeners.forEach { Log.w("Relay", "Relay onClosed $url, $newMessage") } "CLOSED" -> listeners.forEach { Log.w("Relay", "Relay onClosed $url, $newMessage") }
else -> else -> {
RelayStats.newError(url, "Unsupported message: $newMessage")
listeners.forEach { listeners.forEach {
Log.w("Relay", "Unsupported message: $newMessage") Log.w("Relay", "Unsupported message: $newMessage")
it.onError( it.onError(
@@ -345,6 +339,7 @@ class Relay(
Error("Unknown type $type on channel. Msg was $newMessage"), Error("Unknown type $type on channel. Msg was $newMessage"),
) )
} }
}
} }
} }
@@ -389,10 +384,8 @@ class Relay(
it.filter.toJson(url) it.filter.toJson(url)
} }
// Log.d("Relay", "onFilterSent $url $requestId $request") writeToSocket(request)
socket?.send(request)
eventUploadCounterInBytes += request.bytesUsedInMemory()
resetEOSEStatuses() resetEOSEStatuses()
} }
} }
@@ -457,30 +450,9 @@ class Relay(
checkNotInMainThread() checkNotInMainThread()
if (signedEvent is RelayAuthEvent) { if (signedEvent is RelayAuthEvent) {
authResponse.put(signedEvent.id, false) sendAuth(signedEvent)
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
} else { } else {
val event = """["EVENT",${signedEvent.toJson()}]""" sendEvent(signedEvent)
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()
}
}
} }
} }
@@ -488,18 +460,12 @@ class Relay(
checkNotInMainThread() checkNotInMainThread()
if (signedEvent is RelayAuthEvent) { if (signedEvent is RelayAuthEvent) {
authResponse.put(signedEvent.id, false) sendAuth(signedEvent)
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
} else { } else {
if (write) { if (write) {
val event = """["EVENT",${signedEvent.toJson()}]"""
if (isConnected()) { if (isConnected()) {
if (isReady) { if (isReady) {
socket?.send(event) writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
eventUploadCounterInBytes += event.bytesUsedInMemory()
} else { } else {
synchronized(sendWhenReady) { synchronized(sendWhenReady) {
sendWhenReady.add(signedEvent) sendWhenReady.add(signedEvent)
@@ -508,10 +474,7 @@ class Relay(
} else { } else {
// sends all filters after connection is successful. // sends all filters after connection is successful.
connectAndRun { connectAndRun {
checkNotInMainThread() writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
// Sends everything. // Sends everything.
renewFilters() renewFilters()
@@ -521,12 +484,40 @@ class Relay(
} }
} }
fun close(subscriptionId: String) { private fun sendAuth(signedEvent: RelayAuthEvent) {
checkNotInMainThread() authResponse.put(signedEvent.id, false)
writeToSocket("""["AUTH",${signedEvent.toJson()}]""")
}
val msg = """["CLOSE","$subscriptionId"]""" private fun sendEvent(signedEvent: EventInterface) {
// Log.d("Relay", "Close Subscription $url $msg") if (isConnected()) {
socket?.send(msg) 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 { fun isSameRelayConfig(other: Relay): Boolean {
@@ -197,12 +197,6 @@ object RelayPool : Relay.Listener {
afterEOSE: Boolean, afterEOSE: Boolean,
) )
fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
)
fun onRelayStateChange( fun onRelayStateChange(
type: Relay.StateType, type: Relay.StateType,
relay: Relay, relay: Relay,
@@ -241,7 +235,6 @@ object RelayPool : Relay.Listener {
subscriptionId: String, subscriptionId: String,
error: Error, error: Error,
) { ) {
listeners.forEach { it.onError(error, subscriptionId, relay) }
updateStatus() updateStatus()
} }
@@ -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<String, RelayStat>()
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<RelayDebugMessage, RelayDebugMessage>(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,
}
@@ -48,6 +48,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.relays.Constants 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.CloseButton
import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -249,7 +250,7 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) {
OutlinedButton( OutlinedButton(
onClick = { onClick = {
postViewModel.deleteAll() postViewModel.deleteAll()
Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it)) } Constants.defaultSearchRelaySet.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
postViewModel.loadRelayDocuments() postViewModel.loadRelayDocuments()
}, },
) { ) {
@@ -22,14 +22,12 @@ package com.vitorpamplona.amethyst.ui.actions.relays
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.service.relays.RelayStat
@Immutable @Immutable
data class BasicRelaySetupInfo( data class BasicRelaySetupInfo(
val url: String, val url: String,
val errorCount: Int = 0, val relayStat: RelayStat,
val downloadCountInBytes: Int = 0,
val uploadCountInBytes: Int = 0,
val spamCount: Int = 0,
val paidRelay: Boolean = false, val paidRelay: Boolean = false,
) { ) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url) val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
@@ -24,7 +24,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever 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.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -78,20 +78,11 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
val relayList = getRelayList() ?: emptyList() val relayList = getRelayList() ?: emptyList()
relayList.map { relayUrl -> 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( BasicRelaySetupInfo(
relayUrl, relayUrl,
errorCounter, RelayStats.get(relayUrl),
eventDownloadCounter,
eventUploadCounter,
spamCounter,
) )
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed() }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
} }
} }
@@ -28,6 +28,14 @@ fun countToHumanReadableBytes(counter: Int) =
else -> "$counter" 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( fun countToHumanReadable(
counter: Int, counter: Int,
str: String, str: String,
@@ -37,3 +45,13 @@ fun countToHumanReadable(
counter >= 1000 -> "${Math.round(counter / 1000f)}K $str" counter >= 1000 -> "${Math.round(counter / 1000f)}K $str"
else -> "$counter $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"
}
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType 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.actions.RelayInfoDialog
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -151,10 +152,13 @@ fun ServerConfigPreview() {
url = "nostr.mom", url = "nostr.mom",
read = true, read = true,
write = true, write = true,
errorCount = 23, relayStat =
downloadCountInBytes = 10000, RelayStat(
uploadCountInBytes = 10000000, errorCounter = 23,
spamCount = 10, receivedBytes = 10000,
sentBytes = 10000000,
spamCounter = 10,
),
feedTypes = Constants.activeTypesGlobalChats, feedTypes = Constants.activeTypesGlobalChats,
paidRelay = true, paidRelay = true,
), ),
@@ -370,7 +374,7 @@ private fun StatusRow(
) )
Text( Text(
text = countToHumanReadableBytes(item.downloadCountInBytes), text = countToHumanReadableBytes(item.relayStat.receivedBytes),
maxLines = 1, maxLines = 1,
fontSize = 12.sp, fontSize = 12.sp,
modifier = modifier, modifier = modifier,
@@ -406,7 +410,7 @@ private fun StatusRow(
) )
Text( Text(
text = countToHumanReadableBytes(item.uploadCountInBytes), text = countToHumanReadableBytes(item.relayStat.sentBytes),
maxLines = 1, maxLines = 1,
fontSize = 12.sp, fontSize = 12.sp,
modifier = modifier, modifier = modifier,
@@ -432,7 +436,7 @@ private fun StatusRow(
}, },
), ),
tint = tint =
if (item.errorCount > 0) { if (item.relayStat.errorCounter > 0) {
MaterialTheme.colorScheme.warningColor MaterialTheme.colorScheme.warningColor
} else { } else {
MaterialTheme.colorScheme.allGoodColor MaterialTheme.colorScheme.allGoodColor
@@ -440,7 +444,7 @@ private fun StatusRow(
) )
Text( Text(
text = countToHumanReadable(item.errorCount, "errors"), text = countToHumanReadable(item.relayStat.errorCounter, "errors"),
maxLines = 1, maxLines = 1,
fontSize = 12.sp, fontSize = 12.sp,
modifier = modifier, modifier = modifier,
@@ -466,7 +470,7 @@ private fun StatusRow(
}, },
), ),
tint = tint =
if (item.spamCount > 0) { if (item.relayStat.spamCounter > 0) {
MaterialTheme.colorScheme.warningColor MaterialTheme.colorScheme.warningColor
} else { } else {
MaterialTheme.colorScheme.allGoodColor MaterialTheme.colorScheme.allGoodColor
@@ -474,7 +478,7 @@ private fun StatusRow(
) )
Text( Text(
text = countToHumanReadable(item.spamCount, "spam"), text = countToHumanReadable(item.relayStat.spamCounter, "spam"),
maxLines = 1, maxLines = 1,
fontSize = 12.sp, fontSize = 12.sp,
modifier = modifier, modifier = modifier,
@@ -739,7 +743,7 @@ fun Kind3RelayEditBox(
addedWSS, addedWSS,
read, read,
write, write,
feedTypes = FeedType.values().toSet(), feedTypes = FeedType.entries.toSet(),
), ),
) )
url = "" url = ""
@@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.Constants
import com.vitorpamplona.amethyst.service.relays.FeedType 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.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -80,7 +80,6 @@ class Kind3RelayListViewModel : ViewModel() {
if (relayFile != null) { if (relayFile != null) {
relayFile relayFile
.map { .map {
val liveRelay = RelayPool.getRelay(it.key)
val localInfoFeedTypes = val localInfoFeedTypes =
account.localRelays account.localRelays
.filter { localRelay -> localRelay.url == it.key } .filter { localRelay -> localRelay.url == it.key }
@@ -92,48 +91,30 @@ class Kind3RelayListViewModel : ViewModel() {
?.feedTypes ?.feedTypes
?: FeedType.values().toSet().toImmutableSet() ?: 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( RelaySetupInfo(
it.key, it.key,
it.value.read, it.value.read,
it.value.write, it.value.write,
errorCounter, RelayStats.get(it.key),
eventDownloadCounter,
eventUploadCounter,
spamCounter,
localInfoFeedTypes, localInfoFeedTypes,
) )
} }
.distinctBy { it.url } .distinctBy { it.url }
.sortedBy { it.downloadCountInBytes } .sortedBy { it.relayStat.receivedBytes }
.reversed() .reversed()
} else { } else {
account.localRelays account.localRelays
.map { .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( RelaySetupInfo(
it.url, it.url,
it.read, it.read,
it.write, it.write,
errorCounter, RelayStats.get(it.url),
eventDownloadCounter,
eventUploadCounter,
spamCounter,
it.feedTypes, it.feedTypes,
) )
} }
.distinctBy { it.url } .distinctBy { it.url }
.sortedBy { it.downloadCountInBytes } .sortedBy { it.relayStat.receivedBytes }
.reversed() .reversed()
} }
} }
@@ -24,7 +24,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever 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 com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -105,40 +105,22 @@ class Nip65RelayListViewModel : ViewModel() {
val relayList = account.getNIP65RelayList()?.writeRelays() ?: emptyList() val relayList = account.getNIP65RelayList()?.writeRelays() ?: emptyList()
relayList.map { relayUrl -> 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( BasicRelaySetupInfo(
relayUrl, relayUrl,
errorCounter, RelayStats.get(relayUrl),
eventDownloadCounter,
eventUploadCounter,
spamCounter,
) )
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed() }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
} }
_notificationRelays.update { _notificationRelays.update {
val relayList = account.getNIP65RelayList()?.readRelays() ?: emptyList() val relayList = account.getNIP65RelayList()?.readRelays() ?: emptyList()
relayList.map { relayUrl -> 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( BasicRelaySetupInfo(
relayUrl, relayUrl,
errorCounter, RelayStats.get(relayUrl),
eventDownloadCounter,
eventUploadCounter,
spamCounter,
) )
}.distinctBy { it.url }.sortedBy { it.downloadCountInBytes }.reversed() }.distinctBy { it.url }.sortedBy { it.relayStat.receivedBytes }.reversed()
} }
} }
@@ -75,7 +75,7 @@ fun RelayStatusRow(
) )
Text( Text(
text = countToHumanReadableBytes(item.downloadCountInBytes), text = countToHumanReadableBytes(item.relayStat.receivedBytes),
maxLines = 1, maxLines = 1,
fontSize = 12.sp, fontSize = 12.sp,
modifier = modifier, modifier = modifier,
@@ -101,7 +101,7 @@ fun RelayStatusRow(
) )
Text( Text(
text = countToHumanReadableBytes(item.uploadCountInBytes), text = countToHumanReadableBytes(item.relayStat.sentBytes),
maxLines = 1, maxLines = 1,
fontSize = 12.sp, fontSize = 12.sp,
modifier = modifier, modifier = modifier,
@@ -124,7 +124,7 @@ fun RelayStatusRow(
}, },
), ),
tint = tint =
if (item.errorCount > 0) { if (item.relayStat.errorCounter > 0) {
MaterialTheme.colorScheme.warningColor MaterialTheme.colorScheme.warningColor
} else { } else {
MaterialTheme.colorScheme.allGoodColor MaterialTheme.colorScheme.allGoodColor
@@ -132,7 +132,7 @@ fun RelayStatusRow(
) )
Text( Text(
text = countToHumanReadable(item.errorCount, "errors"), text = countToHumanReadable(item.relayStat.errorCounter, "errors"),
maxLines = 1, maxLines = 1,
fontSize = 12.sp, fontSize = 12.sp,
modifier = modifier, modifier = modifier,
@@ -163,7 +163,7 @@ fun RelayStatusRow(
}, },
), ),
tint = tint =
if (item.spamCount > 0) { if (item.relayStat.spamCounter > 0) {
MaterialTheme.colorScheme.warningColor MaterialTheme.colorScheme.warningColor
} else { } else {
MaterialTheme.colorScheme.allGoodColor MaterialTheme.colorScheme.allGoodColor
@@ -171,7 +171,7 @@ fun RelayStatusRow(
) )
Text( Text(
text = countToHumanReadable(item.spamCount, "spam"), text = countToHumanReadable(item.relayStat.spamCounter, "spam"),
maxLines = 1, maxLines = 1,
fontSize = 12.sp, fontSize = 12.sp,
modifier = modifier, modifier = modifier,
@@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R 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.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -65,8 +66,7 @@ fun RelayUrlEditField(onNewRelay: (BasicRelaySetupInfo) -> Unit) {
Button( Button(
onClick = { onClick = {
if (url.isNotBlank() && url != "/") { if (url.isNotBlank() && url != "/") {
val addedWSS = RelayUrlFormatter.normalize(url) onNewRelay(BasicRelaySetupInfo(RelayUrlFormatter.normalize(url), RelayStat()))
onNewRelay(BasicRelaySetupInfo(addedWSS))
url = "" url = ""
} }
}, },