Separates Typed Relay class from a SimpleRelay class to prepare to move to Quartz
This commit is contained in:
@@ -55,7 +55,7 @@ import kotlin.time.measureTimedValue
|
|||||||
class Amethyst : Application() {
|
class Amethyst : Application() {
|
||||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
val client: NostrClient = NostrClient(OkHttpWebSocket.Builder())
|
val client: NostrClient = NostrClient(OkHttpWebSocket.BuilderFactory())
|
||||||
|
|
||||||
// Service Manager is only active when the activity is active.
|
// Service Manager is only active when the activity is active.
|
||||||
val serviceManager = ServiceManager(client, applicationIOScope)
|
val serviceManager = ServiceManager(client, applicationIOScope)
|
||||||
|
|||||||
@@ -2456,9 +2456,9 @@ object LocalCache {
|
|||||||
|
|
||||||
if (event is AddressableEvent && relay != null) {
|
if (event is AddressableEvent && relay != null) {
|
||||||
// updates relay with a new event.
|
// updates relay with a new event.
|
||||||
getAddressableNoteIfExists(event.addressTag())?.let {
|
getAddressableNoteIfExists(event.addressTag())?.let { note ->
|
||||||
it.event?.let { existingEvent ->
|
note.event?.let { existingEvent ->
|
||||||
if (existingEvent.createdAt > event.createdAt) {
|
if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay)) {
|
||||||
Log.d("LocalCache", "Updating ${relay.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}")
|
Log.d("LocalCache", "Updating ${relay.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}")
|
||||||
relay.send(existingEvent)
|
relay.send(existingEvent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -433,6 +433,8 @@ open class Note(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasRelay(relay: Relay) = relay.brief !in relays
|
||||||
|
|
||||||
fun addRelay(relay: Relay) {
|
fun addRelay(relay: Relay) {
|
||||||
if (relay.brief !in relays) {
|
if (relay.brief !in relays) {
|
||||||
addRelaySync(relay.brief)
|
addRelaySync(relay.brief)
|
||||||
|
|||||||
@@ -307,9 +307,9 @@ class User(
|
|||||||
relay: Relay,
|
relay: Relay,
|
||||||
eventTime: Long,
|
eventTime: Long,
|
||||||
) {
|
) {
|
||||||
val here = relaysBeingUsed[relay.url]
|
val here = relaysBeingUsed[relay.brief.url]
|
||||||
if (here == null) {
|
if (here == null) {
|
||||||
relaysBeingUsed = relaysBeingUsed + Pair(relay.url, RelayInfo(relay.url, eventTime, 1))
|
relaysBeingUsed = relaysBeingUsed + Pair(relay.brief.url, RelayInfo(relay.brief.url, eventTime, 1))
|
||||||
} else {
|
} else {
|
||||||
if (eventTime > here.lastEvent) {
|
if (eventTime > here.lastEvent) {
|
||||||
here.lastEvent = eventTime
|
here.lastEvent = eventTime
|
||||||
|
|||||||
+11
-5
@@ -20,9 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.okhttp
|
package com.vitorpamplona.amethyst.service.okhttp
|
||||||
|
|
||||||
import com.vitorpamplona.ammolite.sockets.WebSocket
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebSocket
|
||||||
import com.vitorpamplona.ammolite.sockets.WebSocketListener
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebSocketListener
|
||||||
import com.vitorpamplona.ammolite.sockets.WebsocketBuilder
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebsocketBuilder
|
||||||
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebsocketBuilderFactory
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
@@ -73,14 +74,19 @@ class OkHttpWebSocket(
|
|||||||
) = out.onFailure(t, response?.message)
|
) = out.onFailure(t, response?.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Builder : WebsocketBuilder {
|
class Builder(
|
||||||
|
val forceProxy: Boolean,
|
||||||
|
) : WebsocketBuilder {
|
||||||
override fun build(
|
override fun build(
|
||||||
url: String,
|
url: String,
|
||||||
forceProxy: Boolean,
|
|
||||||
out: WebSocketListener,
|
out: WebSocketListener,
|
||||||
) = OkHttpWebSocket(url, forceProxy, out)
|
) = OkHttpWebSocket(url, forceProxy, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BuilderFactory : WebsocketBuilderFactory {
|
||||||
|
override fun build(forceProxy: Boolean) = Builder(forceProxy)
|
||||||
|
}
|
||||||
|
|
||||||
override fun cancel() {
|
override fun cancel() {
|
||||||
socket?.cancel()
|
socket?.cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
import com.vitorpamplona.ammolite.relays.RelayStat
|
import com.vitorpamplona.ammolite.relays.relays.RelayStat
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
import com.vitorpamplona.ammolite.relays.RelayStat
|
import com.vitorpamplona.ammolite.relays.relays.RelayStat
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||||
import com.vitorpamplona.ammolite.relays.Constants
|
import com.vitorpamplona.ammolite.relays.Constants
|
||||||
import com.vitorpamplona.ammolite.relays.RelayStat
|
import com.vitorpamplona.ammolite.relays.relays.RelayStat
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AllRelayListView(
|
fun AllRelayListView(
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.actions.relays
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.ammolite.relays.FeedType
|
import com.vitorpamplona.ammolite.relays.FeedType
|
||||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||||
import com.vitorpamplona.ammolite.relays.RelayStat
|
import com.vitorpamplona.ammolite.relays.relays.RelayStat
|
||||||
import com.vitorpamplona.quartz.nip01Core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.HexKey
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ import com.vitorpamplona.ammolite.relays.Constants
|
|||||||
import com.vitorpamplona.ammolite.relays.Constants.activeTypesGlobalChats
|
import com.vitorpamplona.ammolite.relays.Constants.activeTypesGlobalChats
|
||||||
import com.vitorpamplona.ammolite.relays.FeedType
|
import com.vitorpamplona.ammolite.relays.FeedType
|
||||||
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
|
||||||
import com.vitorpamplona.ammolite.relays.RelayStat
|
import com.vitorpamplona.ammolite.relays.relays.RelayStat
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||||
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
|
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
|
||||||
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
||||||
import com.vitorpamplona.ammolite.relays.RelayStat
|
import com.vitorpamplona.ammolite.relays.relays.RelayStat
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
|||||||
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
|
||||||
import com.vitorpamplona.ammolite.relays.RelayStat
|
import com.vitorpamplona.ammolite.relays.relays.RelayStat
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ package com.vitorpamplona.ammolite.relays
|
|||||||
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
|
val activeTypesFollows = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
|
||||||
val activeTypesChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS)
|
val activeTypesChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS)
|
||||||
val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
|
val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
|
||||||
val activeTypesSearch = setOf(FeedType.SEARCH)
|
val activeTypesSearch = setOf(FeedType.SEARCH)
|
||||||
@@ -35,7 +35,7 @@ object Constants {
|
|||||||
RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.nostr.bg"), read = true, write = true, feedTypes = activeTypesChats),
|
RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.nostr.bg"), read = true, write = true, feedTypes = activeTypesChats),
|
||||||
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.oxtr.dev"), read = true, write = true, feedTypes = activeTypesChats),
|
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.oxtr.dev"), read = true, write = true, feedTypes = activeTypesChats),
|
||||||
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.fmt.wiz.biz"), read = true, write = false, feedTypes = activeTypesChats),
|
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.fmt.wiz.biz"), read = true, write = false, feedTypes = activeTypesChats),
|
||||||
RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.damus.io"), read = true, write = true, feedTypes = activeTypes),
|
RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.damus.io"), read = true, write = true, feedTypes = activeTypesFollows),
|
||||||
// Global
|
// Global
|
||||||
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.mom"), read = true, write = true, feedTypes = activeTypesGlobalChats),
|
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.mom"), read = true, write = true, feedTypes = activeTypesGlobalChats),
|
||||||
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nos.lol"), read = true, write = true, feedTypes = activeTypesGlobalChats),
|
RelaySetupInfo(RelayUrlFormatter.normalize("wss://nos.lol"), read = true, write = true, feedTypes = activeTypesGlobalChats),
|
||||||
|
|||||||
@@ -21,8 +21,9 @@
|
|||||||
package com.vitorpamplona.ammolite.relays
|
package com.vitorpamplona.ammolite.relays
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.ammolite.relays.relays.RelayState
|
||||||
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebsocketBuilderFactory
|
||||||
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.ammolite.sockets.WebsocketBuilder
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -38,7 +39,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
* published through multiple relays. Events are stored with their respective persona.
|
* published through multiple relays. Events are stored with their respective persona.
|
||||||
*/
|
*/
|
||||||
class NostrClient(
|
class NostrClient(
|
||||||
private val websocketBuilder: WebsocketBuilder,
|
private val websocketBuilder: WebsocketBuilderFactory,
|
||||||
) : RelayPool.Listener {
|
) : RelayPool.Listener {
|
||||||
private val relayPool: RelayPool = RelayPool()
|
private val relayPool: RelayPool = RelayPool()
|
||||||
private val subscriptions: MutableSubscriptionManager = MutableSubscriptionManager()
|
private val subscriptions: MutableSubscriptionManager = MutableSubscriptionManager()
|
||||||
@@ -174,17 +175,24 @@ class NostrClient(
|
|||||||
Log.d("sendAndWaitForResponse", "onError Error from relay ${relay.url} count: ${latch.count} error: $error")
|
Log.d("sendAndWaitForResponse", "onError Error from relay ${relay.url} count: ${latch.count} error: $error")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRelayStateChange(
|
override fun onEOSE(
|
||||||
type: Relay.StateType,
|
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
subscriptionId: String?,
|
subscriptionId: String,
|
||||||
) {
|
) {
|
||||||
if (type == Relay.StateType.DISCONNECT || type == Relay.StateType.EOSE) {
|
latch.countDown()
|
||||||
|
Log.d("sendAndWaitForResponse", "onEOSE relay ${relay.url} count: ${latch.count}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRelayStateChange(
|
||||||
|
type: RelayState,
|
||||||
|
relay: Relay,
|
||||||
|
) {
|
||||||
|
if (type == RelayState.DISCONNECTED) {
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
if (type == Relay.StateType.CONNECT) {
|
if (type == RelayState.CONNECTED) {
|
||||||
Log.d("sendAndWaitForResponse", "${type.name} Sending event to relay ${relay.url} count: ${latch.count}")
|
Log.d("sendAndWaitForResponse", "${type.name} Sending event to relay ${relay.url} count: ${latch.count}")
|
||||||
relay.sendOverride(signedEvent)
|
relay.send(signedEvent)
|
||||||
}
|
}
|
||||||
Log.d("sendAndWaitForResponse", "onRelayStateChange ${type.name} from relay ${relay.url} count: ${latch.count}")
|
Log.d("sendAndWaitForResponse", "onRelayStateChange ${type.name} from relay ${relay.url} count: ${latch.count}")
|
||||||
}
|
}
|
||||||
@@ -312,16 +320,18 @@ class NostrClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRelayStateChange(
|
override fun onEOSE(
|
||||||
type: Relay.StateType,
|
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
channel: String?,
|
subscriptionId: String,
|
||||||
) {
|
) {
|
||||||
// Releases the Web thread for the new payload.
|
listeners.forEach { it.onEOSE(relay, subscriptionId) }
|
||||||
// May need to add a processing queue if processing new events become too costly.
|
}
|
||||||
// GlobalScope.launch(Dispatchers.Default) {
|
|
||||||
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
|
override fun onRelayStateChange(
|
||||||
// }
|
type: RelayState,
|
||||||
|
relay: Relay,
|
||||||
|
) {
|
||||||
|
listeners.forEach { it.onRelayStateChange(type, relay) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
@@ -420,10 +430,15 @@ class NostrClient(
|
|||||||
) = Unit
|
) = Unit
|
||||||
|
|
||||||
/** Connected to or disconnected from a relay */
|
/** Connected to or disconnected from a relay */
|
||||||
open fun onRelayStateChange(
|
open fun onEOSE(
|
||||||
type: Relay.StateType,
|
relay: Relay,
|
||||||
|
subscriptionId: String,
|
||||||
|
) = Unit
|
||||||
|
|
||||||
|
/** Connected to or disconnected from a relay */
|
||||||
|
open fun onRelayStateChange(
|
||||||
|
type: RelayState,
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
subscriptionId: String?,
|
|
||||||
) = Unit
|
) = Unit
|
||||||
|
|
||||||
/** When an relay saves or rejects a new event. */
|
/** When an relay saves or rejects a new event. */
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.ammolite.relays
|
package com.vitorpamplona.ammolite.relays
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.ammolite.relays.relays.RelayState
|
||||||
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
@@ -93,25 +94,20 @@ abstract class NostrDataSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRelayStateChange(
|
override fun onEOSE(
|
||||||
type: Relay.StateType,
|
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
subscriptionId: String?,
|
subscriptionId: String,
|
||||||
) {
|
) {
|
||||||
// if (subscriptions.containsKey(subscriptionId)) {
|
if (subscriptions.containsKey(subscriptionId)) {
|
||||||
// Log.d(this@NostrDataSource.javaClass.simpleName, "Relay ${relay.url} ${subscriptionId}
|
|
||||||
// ${type.name}")
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (
|
|
||||||
type == Relay.StateType.EOSE &&
|
|
||||||
subscriptionId != null &&
|
|
||||||
subscriptions.containsKey(subscriptionId)
|
|
||||||
) {
|
|
||||||
markAsEOSE(subscriptionId, relay)
|
markAsEOSE(subscriptionId, relay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRelayStateChange(
|
||||||
|
type: RelayState,
|
||||||
|
relay: Relay,
|
||||||
|
) {}
|
||||||
|
|
||||||
override fun onSendResponse(
|
override fun onSendResponse(
|
||||||
eventId: String,
|
eventId: String,
|
||||||
success: Boolean,
|
success: Boolean,
|
||||||
|
|||||||
@@ -20,20 +20,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.ammolite.relays
|
package com.vitorpamplona.ammolite.relays
|
||||||
|
|
||||||
import android.util.Log
|
import com.vitorpamplona.ammolite.relays.relays.RelayState
|
||||||
import com.vitorpamplona.ammolite.BuildConfig
|
import com.vitorpamplona.ammolite.relays.relays.SimpleRelay
|
||||||
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
import com.vitorpamplona.ammolite.relays.relays.Subscription
|
||||||
import com.vitorpamplona.ammolite.sockets.WebSocket
|
import com.vitorpamplona.ammolite.relays.relays.SubscriptionCollection
|
||||||
import com.vitorpamplona.ammolite.sockets.WebSocketListener
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebsocketBuilderFactory
|
||||||
import com.vitorpamplona.ammolite.sockets.WebsocketBuilder
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.HexKey
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
import com.vitorpamplona.quartz.nip01Core.relays.Filter
|
||||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
|
||||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
enum class FeedType {
|
enum class FeedType {
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
@@ -53,35 +47,50 @@ val COMMON_FEED_TYPES =
|
|||||||
val EVENT_FINDER_TYPES =
|
val EVENT_FINDER_TYPES =
|
||||||
setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL)
|
setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL)
|
||||||
|
|
||||||
|
class RelaySubFilter(
|
||||||
|
val url: String,
|
||||||
|
val activeTypes: Set<FeedType>,
|
||||||
|
val subs: SubscriptionManager,
|
||||||
|
) : SubscriptionCollection {
|
||||||
|
override fun allSubscriptions(): List<Subscription> =
|
||||||
|
subs.allSubscriptions().mapNotNull { filter ->
|
||||||
|
val filters = filter(filter.value)
|
||||||
|
if (filters.isNotEmpty()) {
|
||||||
|
Subscription(filter.key, filters)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filter(filters: List<TypedFilter>): List<Filter> =
|
||||||
|
filters.mapNotNull { filter ->
|
||||||
|
if (activeTypes.any { it in filter.types } && filter.filter.isValidFor(url)) {
|
||||||
|
filter.filter.toRelay(url)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Relay(
|
class Relay(
|
||||||
val url: String,
|
val url: String,
|
||||||
val read: Boolean = true,
|
val read: Boolean = true,
|
||||||
val write: Boolean = true,
|
val write: Boolean = true,
|
||||||
val forceProxy: Boolean = false,
|
val forceProxy: Boolean = false,
|
||||||
val activeTypes: Set<FeedType>,
|
val activeTypes: Set<FeedType>,
|
||||||
val socketBuilder: WebsocketBuilder,
|
socketBuilderFactory: WebsocketBuilderFactory,
|
||||||
val subs: SubscriptionManager,
|
subs: SubscriptionManager,
|
||||||
) {
|
) : SimpleRelay.Listener {
|
||||||
companion object {
|
private var listeners = setOf<Listener>()
|
||||||
// waits 3 minutes to reconnect once things fail
|
|
||||||
const val RECONNECTING_IN_SECONDS = 60 * 3
|
val relaySubFilter = RelaySubFilter(url, activeTypes, subs)
|
||||||
}
|
val inner =
|
||||||
|
SimpleRelay(url, socketBuilderFactory.build(forceProxy), relaySubFilter, RelayStats.get(url)).apply {
|
||||||
|
register(this@Relay)
|
||||||
|
}
|
||||||
|
|
||||||
val brief = RelayBriefInfoCache.get(url)
|
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
|
|
||||||
|
|
||||||
private var lastConnectTentative: Long = 0L
|
|
||||||
|
|
||||||
private var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
|
|
||||||
|
|
||||||
private val authResponse = mutableMapOf<HexKey, Boolean>()
|
|
||||||
private val authChallengesSent = mutableSetOf<String>()
|
|
||||||
private val outboxCache = mutableMapOf<HexKey, Event>()
|
|
||||||
|
|
||||||
fun register(listener: Listener) {
|
fun register(listener: Listener) {
|
||||||
listeners = listeners.plus(listener)
|
listeners = listeners.plus(listener)
|
||||||
}
|
}
|
||||||
@@ -90,460 +99,43 @@ class Relay(
|
|||||||
listeners = listeners.minus(listener)
|
listeners = listeners.minus(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isConnected(): Boolean = socket != null
|
fun isConnected() = inner.isConnected()
|
||||||
|
|
||||||
fun connect() {
|
fun connect() = inner.connect()
|
||||||
connectAndRun {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
// Sends everything.
|
fun connectAndRun(onConnected: () -> Unit) {
|
||||||
renewFilters()
|
|
||||||
sendOutbox()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendOutbox() {
|
|
||||||
synchronized(outboxCache) {
|
|
||||||
outboxCache.values.forEach {
|
|
||||||
send(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var connectingBlock = AtomicBoolean()
|
|
||||||
|
|
||||||
fun connectAndRun(onConnected: (Relay) -> Unit) {
|
|
||||||
Log.d("Relay", "Relay.connect $url proxy: $forceProxy 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
|
||||||
|
|
||||||
// If there is a connection, don't wait.
|
inner.connectAndRun(onConnected)
|
||||||
if (connectingBlock.getAndSet(true)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
if (socket != null) {
|
|
||||||
connectingBlock.set(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastConnectTentative = TimeUtils.now()
|
|
||||||
|
|
||||||
socket = socketBuilder.build(url, forceProxy, RelayListener(onConnected))
|
|
||||||
socket?.connect()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
|
|
||||||
RelayStats.newError(url, e.message ?: "Error trying to connect: ${e.javaClass.simpleName}")
|
|
||||||
|
|
||||||
markConnectionAsClosed()
|
|
||||||
e.printStackTrace()
|
|
||||||
} finally {
|
|
||||||
connectingBlock.set(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class RelayListener(
|
fun sendOutbox() = inner.sendOutbox()
|
||||||
val onConnected: (Relay) -> Unit,
|
|
||||||
) : WebSocketListener {
|
|
||||||
override fun onOpen(
|
|
||||||
pingMillis: Long,
|
|
||||||
compression: Boolean,
|
|
||||||
) {
|
|
||||||
checkNotInMainThread()
|
|
||||||
Log.d("Relay", "Connect onOpen $url $socket")
|
|
||||||
|
|
||||||
markConnectionAsReady(pingMillis, compression)
|
fun disconnect() = inner.disconnect()
|
||||||
|
|
||||||
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
|
|
||||||
onConnected(this@Relay)
|
|
||||||
|
|
||||||
listeners.forEach { it.onRelayStateChange(this@Relay, StateType.CONNECT, null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(text: String) {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
RelayStats.addBytesReceived(url, text.bytesUsedInMemory())
|
|
||||||
|
|
||||||
try {
|
|
||||||
processNewRelayMessage(text)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
e.printStackTrace()
|
|
||||||
text.chunked(2000) { chunked ->
|
|
||||||
listeners.forEach { it.onError(this@Relay, "", Error("Problem with $chunked")) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosing(
|
|
||||||
code: Int,
|
|
||||||
reason: String,
|
|
||||||
) {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
Log.w("Relay", "Relay onClosing $url: $reason")
|
|
||||||
|
|
||||||
listeners.forEach {
|
|
||||||
it.onRelayStateChange(
|
|
||||||
this@Relay,
|
|
||||||
StateType.DISCONNECTING,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosed(
|
|
||||||
code: Int,
|
|
||||||
reason: String,
|
|
||||||
) {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
markConnectionAsClosed()
|
|
||||||
|
|
||||||
Log.w("Relay", "Relay onClosed $url: $reason")
|
|
||||||
|
|
||||||
listeners.forEach { it.onRelayStateChange(this@Relay, StateType.DISCONNECT, null) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(
|
|
||||||
t: Throwable,
|
|
||||||
responseMessage: String?,
|
|
||||||
) {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
socket?.cancel() // 1000, "Normal close"
|
|
||||||
|
|
||||||
// checks if this is an actual failure. Closing the socket generates an onFailure as well.
|
|
||||||
if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) {
|
|
||||||
RelayStats.newError(url, responseMessage ?: t.message ?: "onFailure event from server: ${t.javaClass.simpleName}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failures disconnect the relay.
|
|
||||||
markConnectionAsClosed()
|
|
||||||
|
|
||||||
Log.w("Relay", "Relay onFailure $url, $responseMessage $responseMessage ${t.message} $socket")
|
|
||||||
t.printStackTrace()
|
|
||||||
listeners.forEach {
|
|
||||||
it.onError(
|
|
||||||
this@Relay,
|
|
||||||
"",
|
|
||||||
Error("WebSocket Failure. Response: $responseMessage. Exception: ${t.message}", t),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markConnectionAsReady(
|
|
||||||
pingInMs: Long,
|
|
||||||
usingCompression: Boolean,
|
|
||||||
) {
|
|
||||||
this.resetEOSEStatuses()
|
|
||||||
this.isReady = true
|
|
||||||
this.usingCompression = usingCompression
|
|
||||||
|
|
||||||
RelayStats.setPing(url, pingInMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markConnectionAsClosed() {
|
|
||||||
this.socket = null
|
|
||||||
this.isReady = false
|
|
||||||
this.usingCompression = false
|
|
||||||
this.resetEOSEStatuses()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processNewRelayMessage(newMessage: String) {
|
|
||||||
val msgArray = EventMapper.mapper.readTree(newMessage)
|
|
||||||
|
|
||||||
when (val type = msgArray.get(0).asText()) {
|
|
||||||
"EVENT" -> {
|
|
||||||
val subscriptionId = msgArray.get(1).asText()
|
|
||||||
val event = EventMapper.fromJson(msgArray.get(2))
|
|
||||||
|
|
||||||
// Log.w("Relay", "Relay onEVENT ${event.kind} $url, $subscriptionId ${msgArray.get(2)}")
|
|
||||||
|
|
||||||
listeners.forEach {
|
|
||||||
it.onEvent(
|
|
||||||
this@Relay,
|
|
||||||
subscriptionId,
|
|
||||||
event,
|
|
||||||
afterEOSEPerSubscription[subscriptionId] == true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"EOSE" ->
|
|
||||||
listeners.forEach {
|
|
||||||
val subscriptionId = msgArray.get(1).asText()
|
|
||||||
|
|
||||||
afterEOSEPerSubscription[subscriptionId] = true
|
|
||||||
// Log.w("Relay", "Relay onEOSE $url $subscriptionId")
|
|
||||||
it.onRelayStateChange(this@Relay, StateType.EOSE, subscriptionId)
|
|
||||||
}
|
|
||||||
"NOTICE" ->
|
|
||||||
listeners.forEach {
|
|
||||||
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" ->
|
|
||||||
listeners.forEach {
|
|
||||||
val eventId = msgArray[1].asText()
|
|
||||||
val success = msgArray[2].asBoolean()
|
|
||||||
val message = if (msgArray.size() > 2) msgArray[3].asText() else ""
|
|
||||||
|
|
||||||
Log.w("Relay", "Relay on OK $url, $eventId, $success, $message")
|
|
||||||
|
|
||||||
if (authResponse.containsKey(eventId)) {
|
|
||||||
val wasAlreadyAuthenticated = authResponse.get(eventId)
|
|
||||||
authResponse.put(eventId, success)
|
|
||||||
if (wasAlreadyAuthenticated != true && success) {
|
|
||||||
renewFilters()
|
|
||||||
sendOutbox()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outboxCache.contains(eventId) && !message.startsWith("auth-required")) {
|
|
||||||
synchronized(outboxCache) {
|
|
||||||
outboxCache.remove(eventId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
RelayStats.newNotice(url, "Failed to receive $eventId: $message")
|
|
||||||
}
|
|
||||||
|
|
||||||
it.onSendResponse(this@Relay, eventId, success, message)
|
|
||||||
}
|
|
||||||
"AUTH" ->
|
|
||||||
listeners.forEach {
|
|
||||||
Log.w("Relay", "Relay onAuth $url, ${ msgArray[1].asText()}")
|
|
||||||
it.onAuth(this@Relay, msgArray[1].asText())
|
|
||||||
}
|
|
||||||
"NOTIFY" ->
|
|
||||||
listeners.forEach {
|
|
||||||
// Log.w("Relay", "Relay onNotify $url, ${msg[1].asString}")
|
|
||||||
it.onNotify(this@Relay, msgArray[1].asText())
|
|
||||||
}
|
|
||||||
"CLOSED" -> listeners.forEach { Log.w("Relay", "Relay Closed Subscription $url, $newMessage") }
|
|
||||||
else -> {
|
|
||||||
RelayStats.newError(url, "Unsupported message: $newMessage")
|
|
||||||
|
|
||||||
listeners.forEach {
|
|
||||||
Log.w("Relay", "Unsupported message: $newMessage")
|
|
||||||
it.onError(
|
|
||||||
this@Relay,
|
|
||||||
"",
|
|
||||||
Error("Unknown type $type on channel. Msg was $newMessage"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disconnect() {
|
|
||||||
Log.d("Relay", "Relay.disconnect $url")
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
lastConnectTentative = 0L // this is not an error, so prepare to reconnect as soon as requested.
|
|
||||||
socket?.cancel()
|
|
||||||
socket = null
|
|
||||||
isReady = false
|
|
||||||
usingCompression = false
|
|
||||||
resetEOSEStatuses()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetEOSEStatuses() {
|
|
||||||
afterEOSEPerSubscription = LinkedHashMap(afterEOSEPerSubscription.size)
|
|
||||||
|
|
||||||
authResponse.clear()
|
|
||||||
authChallengesSent.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendFilter(
|
fun sendFilter(
|
||||||
requestId: String,
|
requestId: String,
|
||||||
filters: List<TypedFilter>,
|
filters: List<TypedFilter>,
|
||||||
) {
|
) {
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
if (read) {
|
if (read) {
|
||||||
if (isConnected()) {
|
inner.sendFilter(requestId, relaySubFilter.filter(filters))
|
||||||
if (isReady) {
|
|
||||||
val relayFilters =
|
|
||||||
filters.filter { filter ->
|
|
||||||
activeTypes.any { it in filter.types } && filter.filter.isValidFor(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relayFilters.isNotEmpty()) {
|
|
||||||
writeToSocket(
|
|
||||||
relayFilters.joinToStringLimited(
|
|
||||||
separator = ",",
|
|
||||||
limit = 19,
|
|
||||||
prefix = """["REQ","$requestId",""",
|
|
||||||
postfix = "]",
|
|
||||||
) {
|
|
||||||
it.filter.toJson(url)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
afterEOSEPerSubscription[requestId] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// waits 60 seconds to reconnect after disconnected.
|
|
||||||
if (TimeUtils.now() > lastConnectTentative + RECONNECTING_IN_SECONDS) {
|
|
||||||
// sends all filters after connection is successful.
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Iterable<T>.joinToStringLimited(
|
fun connectAndSendFiltersIfDisconnected() = inner.connectAndSendFiltersIfDisconnected()
|
||||||
separator: CharSequence = ", ",
|
|
||||||
prefix: CharSequence = "",
|
|
||||||
postfix: CharSequence = "",
|
|
||||||
limit: Int = -1,
|
|
||||||
transform: ((T) -> CharSequence)? = null,
|
|
||||||
): String {
|
|
||||||
val buffer = StringBuilder()
|
|
||||||
buffer.append(prefix)
|
|
||||||
var count = 0
|
|
||||||
for (element in this) {
|
|
||||||
if (limit < 0 || count <= limit) {
|
|
||||||
if (++count > 1) buffer.append(separator)
|
|
||||||
when {
|
|
||||||
transform != null -> buffer.append(transform(element))
|
|
||||||
element is CharSequence? -> buffer.append(element)
|
|
||||||
element is Char -> buffer.append(element)
|
|
||||||
else -> buffer.append(element.toString())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buffer.append(postfix)
|
|
||||||
return buffer.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun connectAndSendFiltersIfDisconnected() {
|
fun renewFilters() = inner.renewFilters()
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
if (socket == null) {
|
fun sendOverride(signedEvent: Event) = inner.send(signedEvent)
|
||||||
// waits 60 seconds to reconnect after disconnected.
|
|
||||||
if (TimeUtils.now() > lastConnectTentative + RECONNECTING_IN_SECONDS) {
|
|
||||||
// println("sendfilter Only if Disconnected ${url} ")
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun renewFilters() {
|
|
||||||
// Force update all filters after AUTH.
|
|
||||||
subs.allSubscriptions().forEach {
|
|
||||||
sendFilter(requestId = it.key, it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function sends the event regardless of the relay being write or not.
|
|
||||||
fun sendOverride(signedEvent: Event) {
|
|
||||||
checkNotInMainThread()
|
|
||||||
|
|
||||||
listeners.forEach { listener ->
|
|
||||||
listener.onBeforeSend(this@Relay, signedEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signedEvent is RelayAuthEvent) {
|
|
||||||
sendAuth(signedEvent)
|
|
||||||
} else {
|
|
||||||
sendEvent(signedEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun send(signedEvent: Event) {
|
fun send(signedEvent: Event) {
|
||||||
checkNotInMainThread()
|
if (signedEvent is RelayAuthEvent || write) {
|
||||||
|
inner.send(signedEvent)
|
||||||
listeners.forEach { listener ->
|
|
||||||
listener.onBeforeSend(this@Relay, signedEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signedEvent is RelayAuthEvent) {
|
|
||||||
sendAuth(signedEvent)
|
|
||||||
} else {
|
|
||||||
if (write) {
|
|
||||||
sendEvent(signedEvent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendAuth(signedEvent: RelayAuthEvent) {
|
fun close(subscriptionId: String) = inner.close(subscriptionId)
|
||||||
val challenge = signedEvent.challenge() ?: ""
|
|
||||||
|
|
||||||
// only send replies to new challenges to avoid infinite loop:
|
|
||||||
// 1. Auth is sent
|
|
||||||
// 2. auth is rejected
|
|
||||||
// 3. auth is requested
|
|
||||||
// 4. auth is sent
|
|
||||||
// ...
|
|
||||||
if (!authChallengesSent.contains(challenge)) {
|
|
||||||
authResponse.put(signedEvent.id, false)
|
|
||||||
authChallengesSent.add(challenge)
|
|
||||||
writeToSocket("""["AUTH",${signedEvent.toJson()}]""")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendEvent(signedEvent: Event) {
|
|
||||||
synchronized(outboxCache) {
|
|
||||||
outboxCache.put(signedEvent.id, signedEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isConnected()) {
|
|
||||||
if (isReady) {
|
|
||||||
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// sends all filters after connection is successful.
|
|
||||||
connectAndRun {
|
|
||||||
// Sends everything.
|
|
||||||
renewFilters()
|
|
||||||
sendOutbox()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun writeToSocket(str: String) {
|
|
||||||
if (socket == null) {
|
|
||||||
listeners.forEach { listener ->
|
|
||||||
listener.onError(
|
|
||||||
this@Relay,
|
|
||||||
"",
|
|
||||||
Error("Failed to send $str. Relay is not connected."),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket?.let {
|
|
||||||
checkNotInMainThread()
|
|
||||||
val result = it.send(str)
|
|
||||||
listeners.forEach { listener ->
|
|
||||||
listener.onSend(this@Relay, str, result)
|
|
||||||
}
|
|
||||||
RelayStats.addBytesSent(url, str.bytesUsedInMemory())
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") {
|
|
||||||
Log.d("Relay", "Relay send $url (${str.length} chars) $str")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close(subscriptionId: String) {
|
|
||||||
writeToSocket("""["CLOSE","$subscriptionId"]""")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isSameRelayConfig(other: RelaySetupInfoToConnect): Boolean =
|
fun isSameRelayConfig(other: RelaySetupInfoToConnect): Boolean =
|
||||||
url == other.url &&
|
url == other.url &&
|
||||||
@@ -552,22 +144,58 @@ class Relay(
|
|||||||
read == other.read &&
|
read == other.read &&
|
||||||
activeTypes == other.feedTypes
|
activeTypes == other.feedTypes
|
||||||
|
|
||||||
enum class StateType {
|
override fun onEvent(
|
||||||
// Websocket connected
|
relay: SimpleRelay,
|
||||||
CONNECT,
|
subscriptionId: String,
|
||||||
|
event: Event,
|
||||||
|
afterEOSE: Boolean,
|
||||||
|
) = listeners.forEach { it.onEvent(this, subscriptionId, event, afterEOSE) }
|
||||||
|
|
||||||
// Websocket disconnecting
|
override fun onError(
|
||||||
DISCONNECTING,
|
relay: SimpleRelay,
|
||||||
|
subscriptionId: String,
|
||||||
|
error: Error,
|
||||||
|
) = listeners.forEach { it.onError(this, subscriptionId, error) }
|
||||||
|
|
||||||
// Websocket disconnected
|
override fun onEOSE(
|
||||||
DISCONNECT,
|
relay: SimpleRelay,
|
||||||
|
subscriptionId: String,
|
||||||
|
) = listeners.forEach { it.onEOSE(this, subscriptionId) }
|
||||||
|
|
||||||
// End Of Stored Events
|
override fun onRelayStateChange(
|
||||||
EOSE,
|
relay: SimpleRelay,
|
||||||
}
|
type: RelayState,
|
||||||
|
) = listeners.forEach { it.onRelayStateChange(this, type) }
|
||||||
|
|
||||||
|
override fun onSendResponse(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
eventId: String,
|
||||||
|
success: Boolean,
|
||||||
|
message: String,
|
||||||
|
) = listeners.forEach { it.onSendResponse(this, eventId, success, message) }
|
||||||
|
|
||||||
|
override fun onAuth(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
challenge: String,
|
||||||
|
) = listeners.forEach { it.onAuth(this, challenge) }
|
||||||
|
|
||||||
|
override fun onNotify(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
description: String,
|
||||||
|
) = listeners.forEach { it.onNotify(this, description) }
|
||||||
|
|
||||||
|
override fun onSend(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
msg: String,
|
||||||
|
success: Boolean,
|
||||||
|
) = listeners.forEach { it.onSend(this, msg, success) }
|
||||||
|
|
||||||
|
override fun onBeforeSend(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
event: Event,
|
||||||
|
) = listeners.forEach { it.onBeforeSend(this, event) }
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/** A new message was received */
|
|
||||||
fun onEvent(
|
fun onEvent(
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
subscriptionId: String,
|
subscriptionId: String,
|
||||||
@@ -575,6 +203,11 @@ class Relay(
|
|||||||
afterEOSE: Boolean,
|
afterEOSE: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun onEOSE(
|
||||||
|
relay: Relay,
|
||||||
|
subscriptionId: String,
|
||||||
|
)
|
||||||
|
|
||||||
fun onError(
|
fun onError(
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
subscriptionId: String,
|
subscriptionId: String,
|
||||||
@@ -593,32 +226,26 @@ class Relay(
|
|||||||
challenge: String,
|
challenge: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Connected to or disconnected from a relay
|
|
||||||
*
|
|
||||||
* @param type is 0 for disconnect and 1 for connect
|
|
||||||
*/
|
|
||||||
fun onRelayStateChange(
|
fun onRelayStateChange(
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
type: StateType,
|
type: RelayState,
|
||||||
channel: String?,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Relay sent an invoice */
|
/** Relay sent a notification */
|
||||||
fun onNotify(
|
fun onNotify(
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
description: String,
|
description: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun onBeforeSend(
|
||||||
|
relay: Relay,
|
||||||
|
event: Event,
|
||||||
|
)
|
||||||
|
|
||||||
fun onSend(
|
fun onSend(
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
msg: String,
|
msg: String,
|
||||||
success: Boolean,
|
success: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun onBeforeSend(
|
|
||||||
relay: Relay,
|
|
||||||
event: Event,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.ammolite.relays
|
package com.vitorpamplona.ammolite.relays
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.ammolite.relays.relays.RelayState
|
||||||
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -173,10 +174,14 @@ class RelayPool : Relay.Listener {
|
|||||||
afterEOSE: Boolean,
|
afterEOSE: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun onRelayStateChange(
|
fun onEOSE(
|
||||||
type: Relay.StateType,
|
relay: Relay,
|
||||||
|
subscriptionId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onRelayStateChange(
|
||||||
|
type: RelayState,
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
channel: String?,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun onSendResponse(
|
fun onSendResponse(
|
||||||
@@ -232,15 +237,19 @@ class RelayPool : Relay.Listener {
|
|||||||
updateStatus()
|
updateStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onEOSE(
|
||||||
|
relay: Relay,
|
||||||
|
subscriptionId: String,
|
||||||
|
) {
|
||||||
|
listeners.forEach { it.onEOSE(relay, subscriptionId) }
|
||||||
|
updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onRelayStateChange(
|
override fun onRelayStateChange(
|
||||||
relay: Relay,
|
relay: Relay,
|
||||||
type: Relay.StateType,
|
type: RelayState,
|
||||||
channel: String?,
|
|
||||||
) {
|
) {
|
||||||
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
|
listeners.forEach { it.onRelayStateChange(type, relay) }
|
||||||
if (type != Relay.StateType.EOSE) {
|
|
||||||
updateStatus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSendResponse(
|
override fun onSendResponse(
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.ammolite.relays
|
package com.vitorpamplona.ammolite.relays
|
||||||
|
|
||||||
import androidx.collection.LruCache
|
import com.vitorpamplona.ammolite.relays.relays.RelayStat
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
|
||||||
|
|
||||||
object RelayStats {
|
object RelayStats {
|
||||||
private val innerCache = mutableMapOf<String, RelayStat>()
|
private val innerCache = mutableMapOf<String, RelayStat>()
|
||||||
@@ -70,67 +69,3 @@ object RelayStats {
|
|||||||
get(url).newSpam(explanation)
|
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: Long) {
|
|
||||||
receivedBytes += bytesUsedInMemory
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addBytesSent(bytesUsedInMemory: Long) {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,8 +21,11 @@
|
|||||||
package com.vitorpamplona.ammolite.relays.filters
|
package com.vitorpamplona.ammolite.relays.filters
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relays.Filter
|
||||||
|
|
||||||
interface IPerRelayFilter {
|
interface IPerRelayFilter {
|
||||||
|
fun toRelay(forRelay: String): Filter
|
||||||
|
|
||||||
fun toJson(forRelay: String): String
|
fun toJson(forRelay: String): String
|
||||||
|
|
||||||
fun match(
|
fun match(
|
||||||
|
|||||||
+3
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory
|
|||||||
import com.vitorpamplona.quartz.nip01Core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relays.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relays.FilterMatcher
|
import com.vitorpamplona.quartz.nip01Core.relays.FilterMatcher
|
||||||
import com.vitorpamplona.quartz.nip01Core.relays.FilterSerializer
|
import com.vitorpamplona.quartz.nip01Core.relays.FilterSerializer
|
||||||
|
|
||||||
@@ -45,6 +46,8 @@ class SinceAuthorPerRelayFilter(
|
|||||||
// don't send it.
|
// don't send it.
|
||||||
override fun isValidFor(forRelay: String) = authors == null || !authors[forRelay].isNullOrEmpty()
|
override fun isValidFor(forRelay: String) = authors == null || !authors[forRelay].isNullOrEmpty()
|
||||||
|
|
||||||
|
override fun toRelay(forRelay: String) = Filter(ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
||||||
|
|
||||||
override fun toJson(forRelay: String): String = FilterSerializer.toJson(ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
override fun toJson(forRelay: String): String = FilterSerializer.toJson(ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
||||||
|
|
||||||
override fun match(
|
override fun match(
|
||||||
|
|||||||
+3
@@ -23,6 +23,7 @@ package com.vitorpamplona.ammolite.relays.filters
|
|||||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory
|
import com.fasterxml.jackson.databind.node.JsonNodeFactory
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relays.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relays.FilterMatcher
|
import com.vitorpamplona.quartz.nip01Core.relays.FilterMatcher
|
||||||
import com.vitorpamplona.quartz.nip01Core.relays.FilterSerializer
|
import com.vitorpamplona.quartz.nip01Core.relays.FilterSerializer
|
||||||
|
|
||||||
@@ -41,6 +42,8 @@ class SincePerRelayFilter(
|
|||||||
) : IPerRelayFilter {
|
) : IPerRelayFilter {
|
||||||
override fun isValidFor(url: String) = true
|
override fun isValidFor(url: String) = true
|
||||||
|
|
||||||
|
override fun toRelay(forRelay: String) = Filter(ids, authors, kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
||||||
|
|
||||||
override fun toJson(forRelay: String) = FilterSerializer.toJson(ids, authors, kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
override fun toJson(forRelay: String) = FilterSerializer.toJson(ids, authors, kinds, tags, since?.get(forRelay)?.time, until, limit, search)
|
||||||
|
|
||||||
override fun match(
|
override fun match(
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.ammolite.relays.relays
|
||||||
|
|
||||||
|
import androidx.collection.LruCache
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
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: Long) {
|
||||||
|
receivedBytes += bytesUsedInMemory
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addBytesSent(bytesUsedInMemory: Long) {
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.ammolite.relays.relays
|
||||||
|
|
||||||
|
enum class RelayState {
|
||||||
|
// Websocket connected
|
||||||
|
CONNECTED,
|
||||||
|
|
||||||
|
// Websocket disconnecting
|
||||||
|
DISCONNECTING,
|
||||||
|
|
||||||
|
// Websocket disconnected
|
||||||
|
DISCONNECTED,
|
||||||
|
}
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.ammolite.relays.relays
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebSocket
|
||||||
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebSocketListener
|
||||||
|
import com.vitorpamplona.ammolite.relays.relays.sockets.WebsocketBuilder
|
||||||
|
import com.vitorpamplona.ammolite.service.checkNotInMainThread
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relays.Filter
|
||||||
|
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||||
|
import com.vitorpamplona.quartz.utils.joinToStringLimited
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
class SimpleRelay(
|
||||||
|
val url: String,
|
||||||
|
val socketBuilder: WebsocketBuilder,
|
||||||
|
val subs: SubscriptionCollection,
|
||||||
|
val stats: RelayStat = RelayStat(),
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
// waits 3 minutes to reconnect once things fail
|
||||||
|
const val RECONNECTING_IN_SECONDS = 60 * 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private var listeners = setOf<Listener>()
|
||||||
|
private var socket: WebSocket? = null
|
||||||
|
private var isReady: Boolean = false
|
||||||
|
private var usingCompression: Boolean = false
|
||||||
|
|
||||||
|
private var lastConnectTentative: Long = 0L
|
||||||
|
|
||||||
|
private var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
|
||||||
|
|
||||||
|
private val authResponse = mutableMapOf<HexKey, Boolean>()
|
||||||
|
private val authChallengesSent = mutableSetOf<String>()
|
||||||
|
private val outboxCache = mutableMapOf<HexKey, Event>()
|
||||||
|
|
||||||
|
private var connectingMutex = AtomicBoolean()
|
||||||
|
|
||||||
|
fun register(listener: Listener) {
|
||||||
|
listeners = listeners.plus(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister(listener: Listener) {
|
||||||
|
listeners = listeners.minus(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isConnected(): Boolean = socket != null
|
||||||
|
|
||||||
|
fun connect() {
|
||||||
|
connectAndRun {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
// Sends everything.
|
||||||
|
renewFilters()
|
||||||
|
sendOutbox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendOutbox() {
|
||||||
|
synchronized(outboxCache) {
|
||||||
|
outboxCache.values.forEach {
|
||||||
|
send(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connectAndRun(onConnected: () -> Unit) {
|
||||||
|
Log.d("Relay", "Relay.connect $url isAlreadyConnecting: ${connectingMutex.get()}")
|
||||||
|
|
||||||
|
// If there is a connection, don't wait.
|
||||||
|
if (connectingMutex.getAndSet(true)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
if (socket != null) {
|
||||||
|
connectingMutex.set(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastConnectTentative = TimeUtils.now()
|
||||||
|
|
||||||
|
socket = socketBuilder.build(url, RelayListener(onConnected))
|
||||||
|
socket?.connect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
|
||||||
|
stats.newError(e.message ?: "Error trying to connect: ${e.javaClass.simpleName}")
|
||||||
|
|
||||||
|
markConnectionAsClosed()
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
connectingMutex.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class RelayListener(
|
||||||
|
val onConnected: () -> Unit,
|
||||||
|
) : WebSocketListener {
|
||||||
|
override fun onOpen(
|
||||||
|
pingMillis: Long,
|
||||||
|
compression: Boolean,
|
||||||
|
) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
Log.d("Relay", "Connect onOpen $url $socket")
|
||||||
|
|
||||||
|
markConnectionAsReady(pingMillis, compression)
|
||||||
|
|
||||||
|
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
|
||||||
|
onConnected()
|
||||||
|
|
||||||
|
listeners.forEach { it.onRelayStateChange(this@SimpleRelay, RelayState.CONNECTED) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(text: String) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
stats.addBytesReceived(text.bytesUsedInMemory())
|
||||||
|
|
||||||
|
try {
|
||||||
|
processNewRelayMessage(text)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
e.printStackTrace()
|
||||||
|
text.chunked(2000) { chunked ->
|
||||||
|
listeners.forEach { it.onError(this@SimpleRelay, "", Error("Problem with $chunked")) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosing(
|
||||||
|
code: Int,
|
||||||
|
reason: String,
|
||||||
|
) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
Log.w("Relay", "Relay onClosing $url: $reason")
|
||||||
|
|
||||||
|
listeners.forEach {
|
||||||
|
it.onRelayStateChange(this@SimpleRelay, RelayState.DISCONNECTING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosed(
|
||||||
|
code: Int,
|
||||||
|
reason: String,
|
||||||
|
) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
markConnectionAsClosed()
|
||||||
|
|
||||||
|
Log.w("Relay", "Relay onClosed $url: $reason")
|
||||||
|
|
||||||
|
listeners.forEach { it.onRelayStateChange(this@SimpleRelay, RelayState.DISCONNECTED) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
t: Throwable,
|
||||||
|
responseMessage: String?,
|
||||||
|
) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
socket?.cancel() // 1000, "Normal close"
|
||||||
|
|
||||||
|
// checks if this is an actual failure. Closing the socket generates an onFailure as well.
|
||||||
|
if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) {
|
||||||
|
stats.newError(responseMessage ?: t.message ?: "onFailure event from server: ${t.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failures disconnect the relay.
|
||||||
|
markConnectionAsClosed()
|
||||||
|
|
||||||
|
Log.w("Relay", "Relay onFailure $url, $responseMessage $responseMessage ${t.message} $socket")
|
||||||
|
t.printStackTrace()
|
||||||
|
listeners.forEach {
|
||||||
|
it.onError(
|
||||||
|
this@SimpleRelay,
|
||||||
|
"",
|
||||||
|
Error("WebSocket Failure. Response: $responseMessage. Exception: ${t.message}", t),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markConnectionAsReady(
|
||||||
|
pingInMs: Long,
|
||||||
|
usingCompression: Boolean,
|
||||||
|
) {
|
||||||
|
this.resetEOSEStatuses()
|
||||||
|
this.isReady = true
|
||||||
|
this.usingCompression = usingCompression
|
||||||
|
|
||||||
|
stats.pingInMs = pingInMs
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markConnectionAsClosed() {
|
||||||
|
this.socket = null
|
||||||
|
this.isReady = false
|
||||||
|
this.usingCompression = false
|
||||||
|
this.resetEOSEStatuses()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processNewRelayMessage(newMessage: String) {
|
||||||
|
val msgArray = EventMapper.mapper.readTree(newMessage)
|
||||||
|
|
||||||
|
when (val type = msgArray.get(0).asText()) {
|
||||||
|
"EVENT" -> {
|
||||||
|
val subscriptionId = msgArray.get(1).asText()
|
||||||
|
val event = EventMapper.fromJson(msgArray.get(2))
|
||||||
|
|
||||||
|
// Log.w("Relay", "Relay onEVENT ${event.kind} $url, $subscriptionId ${msgArray.get(2)}")
|
||||||
|
|
||||||
|
listeners.forEach {
|
||||||
|
it.onEvent(
|
||||||
|
this@SimpleRelay,
|
||||||
|
subscriptionId,
|
||||||
|
event,
|
||||||
|
afterEOSEPerSubscription[subscriptionId] == true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"EOSE" ->
|
||||||
|
listeners.forEach {
|
||||||
|
val subscriptionId = msgArray.get(1).asText()
|
||||||
|
|
||||||
|
afterEOSEPerSubscription[subscriptionId] = true
|
||||||
|
// Log.w("Relay", "Relay onEOSE $url $subscriptionId")
|
||||||
|
it.onEOSE(this@SimpleRelay, subscriptionId)
|
||||||
|
}
|
||||||
|
"NOTICE" ->
|
||||||
|
listeners.forEach {
|
||||||
|
val message = msgArray.get(1).asText()
|
||||||
|
Log.w("Relay", "Relay onNotice $url, $message")
|
||||||
|
|
||||||
|
stats.newNotice(message)
|
||||||
|
|
||||||
|
it.onError(this@SimpleRelay, message, Error("Relay sent notice: $message"))
|
||||||
|
}
|
||||||
|
"OK" ->
|
||||||
|
listeners.forEach {
|
||||||
|
val eventId = msgArray[1].asText()
|
||||||
|
val success = msgArray[2].asBoolean()
|
||||||
|
val message = if (msgArray.size() > 2) msgArray[3].asText() else ""
|
||||||
|
|
||||||
|
Log.w("Relay", "Relay on OK $url, $eventId, $success, $message")
|
||||||
|
|
||||||
|
if (authResponse.containsKey(eventId)) {
|
||||||
|
val wasAlreadyAuthenticated = authResponse.get(eventId)
|
||||||
|
authResponse.put(eventId, success)
|
||||||
|
if (wasAlreadyAuthenticated != true && success) {
|
||||||
|
renewFilters()
|
||||||
|
sendOutbox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outboxCache.contains(eventId) && !message.startsWith("auth-required")) {
|
||||||
|
synchronized(outboxCache) {
|
||||||
|
outboxCache.remove(eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
stats.newNotice("Failed to receive $eventId: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
it.onSendResponse(this@SimpleRelay, eventId, success, message)
|
||||||
|
}
|
||||||
|
"AUTH" ->
|
||||||
|
listeners.forEach {
|
||||||
|
Log.w("Relay", "Relay onAuth $url, ${ msgArray[1].asText()}")
|
||||||
|
it.onAuth(this@SimpleRelay, msgArray[1].asText())
|
||||||
|
}
|
||||||
|
"NOTIFY" ->
|
||||||
|
listeners.forEach {
|
||||||
|
// Log.w("Relay", "Relay onNotify $url, ${msg[1].asString}")
|
||||||
|
it.onNotify(this@SimpleRelay, msgArray[1].asText())
|
||||||
|
}
|
||||||
|
"CLOSED" -> listeners.forEach { Log.w("Relay", "Relay Closed Subscription $url, $newMessage") }
|
||||||
|
else -> {
|
||||||
|
stats.newError("Unsupported message: $newMessage")
|
||||||
|
|
||||||
|
listeners.forEach {
|
||||||
|
Log.w("Relay", "Unsupported message: $newMessage")
|
||||||
|
it.onError(
|
||||||
|
this@SimpleRelay,
|
||||||
|
"",
|
||||||
|
Error("Unknown type $type on channel. Msg was $newMessage"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnect() {
|
||||||
|
Log.d("Relay", "Relay.disconnect $url")
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
lastConnectTentative = 0L // this is not an error, so prepare to reconnect as soon as requested.
|
||||||
|
socket?.cancel()
|
||||||
|
socket = null
|
||||||
|
isReady = false
|
||||||
|
usingCompression = false
|
||||||
|
resetEOSEStatuses()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetEOSEStatuses() {
|
||||||
|
afterEOSEPerSubscription = LinkedHashMap(afterEOSEPerSubscription.size)
|
||||||
|
|
||||||
|
authResponse.clear()
|
||||||
|
authChallengesSent.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendFilter(
|
||||||
|
requestId: String,
|
||||||
|
filters: List<Filter>,
|
||||||
|
) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
if (isConnected()) {
|
||||||
|
if (isReady) {
|
||||||
|
if (filters.isNotEmpty()) {
|
||||||
|
writeToSocket(
|
||||||
|
filters.joinToStringLimited(
|
||||||
|
separator = ",",
|
||||||
|
limit = 19,
|
||||||
|
prefix = """["REQ","$requestId",""",
|
||||||
|
postfix = "]",
|
||||||
|
) {
|
||||||
|
it.toJson()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
afterEOSEPerSubscription[requestId] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// waits 60 seconds to reconnect after disconnected.
|
||||||
|
if (TimeUtils.now() > lastConnectTentative + RECONNECTING_IN_SECONDS) {
|
||||||
|
// sends all filters after connection is successful.
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connectAndSendFiltersIfDisconnected() {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
if (socket == null) {
|
||||||
|
// waits 60 seconds to reconnect after disconnected.
|
||||||
|
if (TimeUtils.now() > lastConnectTentative + RECONNECTING_IN_SECONDS) {
|
||||||
|
// println("sendfilter Only if Disconnected ${url} ")
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renewFilters() {
|
||||||
|
// Force update all filters after AUTH.
|
||||||
|
subs.allSubscriptions().forEach {
|
||||||
|
sendFilter(requestId = it.id, it.filters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(signedEvent: Event) {
|
||||||
|
checkNotInMainThread()
|
||||||
|
|
||||||
|
listeners.forEach { listener ->
|
||||||
|
listener.onBeforeSend(this@SimpleRelay, signedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signedEvent is RelayAuthEvent) {
|
||||||
|
sendAuth(signedEvent)
|
||||||
|
} else {
|
||||||
|
sendEvent(signedEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendAuth(signedEvent: RelayAuthEvent) {
|
||||||
|
val challenge = signedEvent.challenge() ?: ""
|
||||||
|
|
||||||
|
// only send replies to new challenges to avoid infinite loop:
|
||||||
|
// 1. Auth is sent
|
||||||
|
// 2. auth is rejected
|
||||||
|
// 3. auth is requested
|
||||||
|
// 4. auth is sent
|
||||||
|
// ...
|
||||||
|
if (!authChallengesSent.contains(challenge)) {
|
||||||
|
authResponse.put(signedEvent.id, false)
|
||||||
|
authChallengesSent.add(challenge)
|
||||||
|
writeToSocket("""["AUTH",${signedEvent.toJson()}]""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendEvent(signedEvent: Event) {
|
||||||
|
synchronized(outboxCache) {
|
||||||
|
outboxCache.put(signedEvent.id, signedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnected()) {
|
||||||
|
if (isReady) {
|
||||||
|
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// sends all filters after connection is successful.
|
||||||
|
connectAndRun {
|
||||||
|
// Sends everything.
|
||||||
|
renewFilters()
|
||||||
|
sendOutbox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeToSocket(str: String) {
|
||||||
|
if (socket == null) {
|
||||||
|
listeners.forEach { listener ->
|
||||||
|
listener.onError(
|
||||||
|
this@SimpleRelay,
|
||||||
|
"",
|
||||||
|
Error("Failed to send $str. Relay is not connected."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket?.let {
|
||||||
|
checkNotInMainThread()
|
||||||
|
val result = it.send(str)
|
||||||
|
listeners.forEach { listener ->
|
||||||
|
listener.onSend(this@SimpleRelay, str, result)
|
||||||
|
}
|
||||||
|
stats.addBytesSent(str.bytesUsedInMemory())
|
||||||
|
|
||||||
|
Log.d("Relay", "Relay send $url (${str.length} chars) $str")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close(subscriptionId: String) {
|
||||||
|
writeToSocket("""["CLOSE","$subscriptionId"]""")
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onEvent(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
subscriptionId: String,
|
||||||
|
event: Event,
|
||||||
|
afterEOSE: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onEOSE(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
subscriptionId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onError(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
subscriptionId: String,
|
||||||
|
error: Error,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onSendResponse(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
eventId: String,
|
||||||
|
success: Boolean,
|
||||||
|
message: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onAuth(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
challenge: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onRelayStateChange(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
type: RelayState,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Relay sent a notification */
|
||||||
|
fun onNotify(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
description: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onBeforeSend(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
event: Event,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onSend(
|
||||||
|
relay: SimpleRelay,
|
||||||
|
msg: String,
|
||||||
|
success: Boolean,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.ammolite.relays.relays
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relays.Filter
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class Subscription(
|
||||||
|
val id: String = UUID.randomUUID().toString().substring(0, 4),
|
||||||
|
val filters: List<Filter>,
|
||||||
|
)
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.ammolite.relays.relays
|
||||||
|
|
||||||
|
interface SubscriptionCollection {
|
||||||
|
fun allSubscriptions(): List<Subscription>
|
||||||
|
}
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.ammolite.sockets
|
package com.vitorpamplona.ammolite.relays.relays.sockets
|
||||||
|
|
||||||
interface WebSocket {
|
interface WebSocket {
|
||||||
fun connect()
|
fun connect()
|
||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.ammolite.sockets
|
package com.vitorpamplona.ammolite.relays.relays.sockets
|
||||||
|
|
||||||
interface WebSocketListener {
|
interface WebSocketListener {
|
||||||
fun onOpen(
|
fun onOpen(
|
||||||
+1
-2
@@ -18,12 +18,11 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.ammolite.sockets
|
package com.vitorpamplona.ammolite.relays.relays.sockets
|
||||||
|
|
||||||
interface WebsocketBuilder {
|
interface WebsocketBuilder {
|
||||||
fun build(
|
fun build(
|
||||||
url: String,
|
url: String,
|
||||||
forceProxy: Boolean,
|
|
||||||
out: WebSocketListener,
|
out: WebSocketListener,
|
||||||
): WebSocket
|
): WebSocket
|
||||||
}
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.ammolite.relays.relays.sockets
|
||||||
|
|
||||||
|
interface WebsocketBuilderFactory {
|
||||||
|
fun build(forceProxy: Boolean): WebsocketBuilder
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.HexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.addressables.dTag
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
@@ -39,7 +40,7 @@ class LongTextNoteEvent(
|
|||||||
sig: HexKey,
|
sig: HexKey,
|
||||||
) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||||
AddressableEvent {
|
AddressableEvent {
|
||||||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
override fun dTag() = tags.dTag()
|
||||||
|
|
||||||
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
|
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 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.quartz.utils
|
||||||
|
|
||||||
|
fun <T> Iterable<T>.joinToStringLimited(
|
||||||
|
separator: CharSequence,
|
||||||
|
prefix: CharSequence,
|
||||||
|
postfix: CharSequence,
|
||||||
|
limit: Int,
|
||||||
|
transform: ((T) -> CharSequence)?,
|
||||||
|
): String {
|
||||||
|
val buffer = StringBuilder()
|
||||||
|
buffer.append(prefix)
|
||||||
|
var count = 0
|
||||||
|
for (element in this) {
|
||||||
|
if (limit < 0 || count <= limit) {
|
||||||
|
if (++count > 1) buffer.append(separator)
|
||||||
|
when {
|
||||||
|
transform != null -> buffer.append(transform(element))
|
||||||
|
element is CharSequence? -> buffer.append(element)
|
||||||
|
element is Char -> buffer.append(element)
|
||||||
|
else -> buffer.append(element.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.append(postfix)
|
||||||
|
return buffer.toString()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user