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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FeedType>,
|
||||
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 {
|
||||
return if (useProxy) {
|
||||
if (this.defaultHttpClient == null) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -59,38 +59,24 @@ class Relay(
|
||||
val write: Boolean = true,
|
||||
val activeTypes: Set<FeedType> = 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<Listener>()
|
||||
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<String, Boolean>()
|
||||
|
||||
var lastConnectTentative: Long = 0L
|
||||
|
||||
var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
|
||||
|
||||
val authResponse = mutableMapOf<HexKey, Boolean>()
|
||||
val sendWhenReady = mutableListOf<EventInterface>()
|
||||
private val authResponse = mutableMapOf<HexKey, Boolean>()
|
||||
private val sendWhenReady = mutableListOf<EventInterface>()
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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()
|
||||
},
|
||||
) {
|
||||
|
||||
+2
-4
@@ -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)
|
||||
|
||||
+3
-12
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
+15
-11
@@ -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 = ""
|
||||
|
||||
+5
-24
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-23
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ""
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user